From aff0de55ce112dcf7ab44851d194e3f2468a582e Mon Sep 17 00:00:00 2001 From: bradbal Date: Fri, 25 Nov 2022 11:18:13 +1000 Subject: [PATCH 001/241] Fixed neighbour cache recall from merge errror. --- stlearn/tools/microenv/cci/analysis.py | 9 ++--- stlearn/tools/microenv/cci/het_helpers.py | 41 +++++++---------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index 9dd009d9..cfd8b80b 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -652,13 +652,8 @@ def run_cci( if verbose: print("Getting information for CCI counting...") - spot_bcs, cell_data = get_data_for_counting(adata, use_label, mix_mode, all_set) - # ( - # spot_bcs, - # cell_data, - # neighbourhood_bcs, - # neighbourhood_indices, - # ) = get_data_for_counting_OLD(adata, use_label, mix_mode, all_set) + spot_bcs, cell_data = get_data_for_counting(adata, use_label, mix_mode, + all_set) lr_summary = adata.uns["lr_summary"] col_i = 1 if sig_spots else 0 diff --git a/stlearn/tools/microenv/cci/het_helpers.py b/stlearn/tools/microenv/cci/het_helpers.py index 748c4c2b..bf0d07d0 100644 --- a/stlearn/tools/microenv/cci/het_helpers.py +++ b/stlearn/tools/microenv/cci/het_helpers.py @@ -349,8 +349,6 @@ def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): spot_bcs = adata.obs_names.values.astype(str) return spot_bcs, cell_data, neighbourhood_bcs, neighbourhood_indices - -@njit def get_neighbourhoods_FAST( spot_bcs: np.array, spot_neigh_bcs: np.ndarray, @@ -362,43 +360,28 @@ def get_neighbourhoods_FAST( """Gets the neighbourhood information, njit compiled.""" # Determining the neighbour spots used for significance testing # - ### Some initialisation of the lists with correct types for complilation ### - neighbours = List([neigh_indices])[1:] - neighbourhood_bcs = List() # List([ (spot_bcs[0], neigh_bcs) ])[1:] - neighbourhood_indices = List([(0, neigh_indices)])[1:] - - # neighbours = List( numba.int64[:] ) - # neighbourhood_bcs = List((numba.int64, numba.int64[:])) - # neighbourhood_indices = List( (types.unicode_type, types.unicode_type[:]) ) + neighbours, neighbourhood_bcs, neighbourhood_indices = [], [], [] for i in range(spot_neigh_bcs.shape[0]): - neigh_bcs = spot_neigh_bcs[i, :][0].split(",") - # neigh_bcs = neigh_bcs[neigh_bcs != ""] - neigh_bcs_sub = List() - for neigh_bc in neigh_bcs: - if neigh_bc in spot_bcs: - neigh_bcs_sub.append(neigh_bc) + neigh_bcs = np.array(spot_neigh_bcs[i, :][0].split(",")) + neigh_bcs = neigh_bcs[neigh_bcs != ""] - # neigh_bcs_array = np.empty((len(neigh_bcs_sub)), str_dtype) - neigh_bcs_array = np.empty(len(neigh_bcs_sub), dtype=neigh_bcs_sub._dtype) - neigh_indices = np.zeros((len(neigh_bcs_sub)), dtype=np.int64) - for j, neigh_bc in enumerate(neigh_bcs_sub): - neigh_bcs_array[j] = neigh_bc + neigh_bcs_array, neigh_indices = [], [] + neigh_bcs_sub = List() + for j, neigh_bc in enumerate(neigh_bcs): - for k, bc in enumerate(spot_bcs): - if neigh_bc == bc: - neigh_indices[j] = k - break + bc_indices = np.where(spot_bcs == neigh_bc)[0] + if len(bc_indices) > 0: + neigh_bcs_array.append(neigh_bc) + neigh_indices.append(bc_indices[0]) - # neigh_indices[j] = np.where(spot_bcs == neigh_bc)[0][0] - # neigh_bcs_array.append( neigh_bc ) + neigh_bcs_array = np.array(neigh_bcs_array, dtype=str_dtype) + neigh_indices = np.array(neigh_indices, dtype=np.int64) neighbours.append(neigh_indices) neighbourhood_indices.append((i, neigh_indices)) neighbourhood_bcs.append((spot_bcs[i], neigh_bcs_array)) - # neighbourhood_bcs.append((spot_bcs[i], np.asarray(neigh_bcs_sub))) - # return neighbours, neighbourhood_bcs, neighbourhood_indices return List(neighbours), List(neighbourhood_bcs), List(neighbourhood_indices) From 091a75c90fe7718b6077ea9bb32b091c8c9580ed Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Fri, 25 Nov 2022 13:09:14 +1000 Subject: [PATCH 002/241] fix: fix pre-commit --- stlearn/tools/microenv/cci/analysis.py | 3 +-- stlearn/tools/microenv/cci/het_helpers.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index cfd8b80b..adbb2d19 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -652,8 +652,7 @@ def run_cci( if verbose: print("Getting information for CCI counting...") - spot_bcs, cell_data = get_data_for_counting(adata, use_label, mix_mode, - all_set) + spot_bcs, cell_data = get_data_for_counting(adata, use_label, mix_mode, all_set) lr_summary = adata.uns["lr_summary"] col_i = 1 if sig_spots else 0 diff --git a/stlearn/tools/microenv/cci/het_helpers.py b/stlearn/tools/microenv/cci/het_helpers.py index bf0d07d0..8faa2464 100644 --- a/stlearn/tools/microenv/cci/het_helpers.py +++ b/stlearn/tools/microenv/cci/het_helpers.py @@ -349,6 +349,7 @@ def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): spot_bcs = adata.obs_names.values.astype(str) return spot_bcs, cell_data, neighbourhood_bcs, neighbourhood_indices + def get_neighbourhoods_FAST( spot_bcs: np.array, spot_neigh_bcs: np.ndarray, From b9c4e4b64df528b04801893224b6079654c318b1 Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Mon, 28 Nov 2022 09:00:41 +1000 Subject: [PATCH 003/241] update: v4.0.11 --- HISTORY.rst | 2 ++ docs/conf.py | 2 +- docs/index.rst | 2 ++ setup.cfg | 2 +- setup.py | 2 +- stlearn/__init__.py | 2 +- 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 635c3fac..39a6759c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,8 @@ History ======= +0.4.11 (2022-11-25) +------------------ 0.4.10 (2022-11-22) ------------------ 0.4.8 (2022-06-15) diff --git a/docs/conf.py b/docs/conf.py index 0b6f4461..b85f87ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ import os if not os.path.isdir("./_static"): - url = "https://www.dropbox.com/s/akhl2mr9kn0e0lh/download.zip?dl=1" + url = "https://www.dropbox.com/s/yir8x5reso209d2/download.zip?dl=1" os.system("wget " + url) os.system("mv download.zip?dl=1 download.zip") os.system("unzip download.zip") diff --git a/docs/index.rst b/docs/index.rst index 827ec038..c8e2630c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,8 @@ In the new release, we provide the interactive plots: Latest additions ---------------- +.. include:: release_notes/0.4.11.rst + .. include:: release_notes/0.4.6.rst .. include:: release_notes/0.3.2.rst diff --git a/setup.cfg b/setup.cfg index 62d25210..f877626d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.10 +current_version = 0.4.11 commit = True tag = True diff --git a/setup.py b/setup.py index e817b2d4..e728fba4 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/BiomedicalMachineLearning/stLearn", - version="0.4.10", + version="0.4.11", zip_safe=False, ) diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 643583dc..1fc79b20 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -2,7 +2,7 @@ __author__ = """Genomics and Machine Learning lab""" __email__ = "duy.pham@uq.edu.au" -__version__ = "0.4.10" +__version__ = "0.4.11" from . import add From d67a3370c97cd1436c0231cb9e8255eb829a2fca Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Mon, 28 Nov 2022 09:09:47 +1000 Subject: [PATCH 004/241] update: author for tutorials --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index b85f87ed..8d7a3941 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ import os if not os.path.isdir("./_static"): - url = "https://www.dropbox.com/s/yir8x5reso209d2/download.zip?dl=1" + url = "https://www.dropbox.com/s/gwbpu2xct6nbx6a/download.zip?dl=1" os.system("wget " + url) os.system("mv download.zip?dl=1 download.zip") os.system("unzip download.zip") From b7b1170dd83f884224b03be4567f9f74789b0dc8 Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Mon, 28 Nov 2022 10:26:20 +1000 Subject: [PATCH 005/241] fix: support spacerange 2.0 --- stlearn/wrapper/read.py | 98 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index f6cb527f..3bbcac4d 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -11,6 +11,8 @@ import scanpy import scipy import matplotlib.pyplot as plt +from matplotlib.image import imread +import json _QUALITY = Literal["fulres", "hires", "lowres"] _background = ["black", "white"] @@ -81,15 +83,97 @@ def Read10X( Spatial spot coordinates, usable as `basis` by :func:`~scanpy.pl.embedding`. """ - from scanpy import read_visium + path = Path(path) + adata = scanpy.read_10x_h5(path / count_file, genome=genome) - adata = read_visium( - path, - genome=genome, - count_file=count_file, - library_id=library_id, - load_images=load_images, + adata.uns["spatial"] = dict() + + from h5py import File + + with File(path / count_file, mode="r") as f: + attrs = dict(f.attrs) + if library_id is None: + library_id = str(attrs.pop("library_ids")[0], "utf-8") + + adata.uns["spatial"][library_id] = dict() + + tissue_positions_file = ( + path / "spatial/tissue_positions.csv" + if (path / "spatial/tissue_positions.csv").exists() + else path / "spatial/tissue_positions_list.csv" ) + + if load_images: + files = dict( + tissue_positions_file=tissue_positions_file, + scalefactors_json_file=path / "spatial/scalefactors_json.json", + hires_image=path / "spatial/tissue_hires_image.png", + lowres_image=path / "spatial/tissue_lowres_image.png", + ) + + # check if files exists, continue if images are missing + for f in files.values(): + if not f.exists(): + if any(x in str(f) for x in ["hires_image", "lowres_image"]): + logg.warning( + f"You seem to be missing an image file.\n" + f"Could not find '{f}'." + ) + else: + raise OSError(f"Could not find '{f}'") + + adata.uns["spatial"][library_id]["images"] = dict() + for res in ["hires", "lowres"]: + try: + adata.uns["spatial"][library_id]["images"][res] = imread( + str(files[f"{res}_image"]) + ) + except Exception: + raise OSError(f"Could not find '{res}_image'") + + # read json scalefactors + adata.uns["spatial"][library_id]["scalefactors"] = json.loads( + files["scalefactors_json_file"].read_bytes() + ) + + adata.uns["spatial"][library_id]["metadata"] = { + k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) + for k in ("chemistry_description", "software_version") + if k in attrs + } + + # read coordinates + positions = pd.read_csv(files["tissue_positions_file"], header=None) + positions.columns = [ + "barcode", + "in_tissue", + "array_row", + "array_col", + "pxl_col_in_fullres", + "pxl_row_in_fullres", + ] + positions.index = positions["barcode"] + + adata.obs = adata.obs.join(positions, how="left") + + adata.obsm["spatial"] = ( + adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]] + .to_numpy() + .astype(int) + ) + adata.obs.drop( + columns=["barcode", "pxl_row_in_fullres", "pxl_col_in_fullres"], + inplace=True, + ) + + # put image path in uns + if image_path is not None: + # get an absolute path + image_path = str(Path(image_path).resolve()) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( + image_path + ) + adata.var_names_make_unique() if library_id is None: From e1acebc5b09ddf70dc81374466fda38ee000030c Mon Sep 17 00:00:00 2001 From: Duy Pham Date: Tue, 3 Jan 2023 22:13:43 +1000 Subject: [PATCH 006/241] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 8d7a3941..7450e704 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ import os if not os.path.isdir("./_static"): - url = "https://www.dropbox.com/s/gwbpu2xct6nbx6a/download.zip?dl=1" + url = "https://www.dropbox.com/s/3bb749fk68h0lwh/download.zip?dl=1" os.system("wget " + url) os.system("mv download.zip?dl=1 download.zip") os.system("unzip download.zip") From 89abaa9bfada02c60ac1c63d2713d24f1434af39 Mon Sep 17 00:00:00 2001 From: Duy Pham Date: Tue, 3 Jan 2023 22:19:21 +1000 Subject: [PATCH 007/241] Update conf.py --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 7450e704..272a059b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -115,7 +115,7 @@ def setup(app): - app.add_stylesheet("css/theme_override.css") + app.add_css_file("css/theme_override.css") html_theme = "sphinx_rtd_theme" From 10b105a20c8c2cfdcf77a187b80e043a2283f405 Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Wed, 8 Mar 2023 10:32:36 +1000 Subject: [PATCH 008/241] fix: cci and interactive plot and type matplotlib --- stlearn/plotting/cci_plot.py | 5 ++-- stlearn/plotting/cci_plot_helpers.py | 8 +++++-- stlearn/plotting/classes.py | 14 ++++++------ stlearn/plotting/classes_bokeh.py | 34 ++++++++++++++-------------- stlearn/plotting/cluster_plot.py | 2 +- stlearn/plotting/feat_plot.py | 2 +- stlearn/plotting/gene_plot.py | 2 +- stlearn/utils.py | 2 +- stlearn/wrapper/read.py | 4 ++++ 9 files changed, 40 insertions(+), 33 deletions(-) diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 8a87bd18..06d997f8 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -434,7 +434,7 @@ def lr_result_plot( title: Optional["str"] = None, figsize: Optional[Tuple[float, float]] = None, cmap: Optional[str] = "Spectral_r", - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, @@ -919,7 +919,7 @@ def het_plot( cmap: Optional[str] = "Spectral_r", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, @@ -1069,7 +1069,6 @@ def ccinet_plot( # Either plotting overall interactions, or just for a particular LR # int_df, title = get_int_df(adata, lr, use_label, sig_interactions, title) - # Creating the interaction graph # all_set = int_df.index.values int_matrix = int_df.values diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index f282d9f6..a4554ecd 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -417,19 +417,23 @@ def add_arrows_by_edges( def get_int_df(adata, lr, use_label, sig_interactions, title): """Retrieves the relevant interaction count matrix.""" no_title = type(title) == type(None) + labels_ordered = adata.obs[use_label].cat.categories if type(lr) == type(None): # No LR inputted, so just use all int_df = ( adata.uns[f"lr_cci_{use_label}"] if sig_interactions else adata.uns[f"lr_cci_raw_{use_label}"] - ) + )[labels_ordered].loc[labels_ordered] title = "Cell-Cell LR Interactions" if no_title else title else: + + labels_ordered = adata.obs[use_label].cat.categories int_df = ( adata.uns[f"per_lr_cci_{use_label}"][lr] if sig_interactions else adata.uns[f"per_lr_cci_raw_{use_label}"][lr] - ) + )[labels_ordered].loc[labels_ordered] + title = f"Cell-Cell {lr} interactions" if no_title else title return int_df, title diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 91bd3f9f..f2c4a073 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -41,7 +41,7 @@ def __init__( cmap: Optional[str] = "Spectral_r", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, @@ -244,7 +244,7 @@ def __init__( cmap: Optional[str] = "Spectral_r", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, @@ -457,7 +457,7 @@ def __init__( cmap: Optional[str] = "Spectral_r", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, @@ -627,7 +627,7 @@ def __init__( cmap: Optional[str] = "default", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, @@ -981,7 +981,7 @@ def __init__( cmap: Optional[str] = "jet", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, @@ -1135,7 +1135,7 @@ def __init__( cmap: Optional[str] = "Spectral_r", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, @@ -1205,7 +1205,7 @@ def __init__( figsize: Optional[Tuple[float, float]] = None, cmap: Optional[str] = "Spectral_r", list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, diff --git a/stlearn/plotting/classes_bokeh.py b/stlearn/plotting/classes_bokeh.py index cdbc5881..7a75b2df 100644 --- a/stlearn/plotting/classes_bokeh.py +++ b/stlearn/plotting/classes_bokeh.py @@ -75,7 +75,7 @@ def __init__( view = self.image.view(dtype=np.uint8).reshape((self.ydim, self.xdim, 4)) # Copy the RGBA image into view, flipping it so it comes right-side up # with a lower-left origin - view[:, :, :] = np.flipud(np.asarray(img_pillow)) + view[:, :, :] = np.asarray(img_pillow) # Display the 32-bit RGBA image self.dim = max(self.xdim, self.ydim) @@ -159,7 +159,7 @@ def make_fig(self): fig = figure( title=self.gene_select.value, - x_range=(0, self.dim - 150), + x_range=(0, self.dim), y_range=(self.dim, 0), output_backend=self.output_backend.value, name="GenePlot", @@ -173,7 +173,7 @@ def make_fig(self): fig.image_rgba( image=[self.image], x=0, - y=self.xdim, + y=0, dw=self.ydim, dh=self.xdim, global_alpha=self.tissue_alpha.value, @@ -251,11 +251,11 @@ def add_violin(self): view2 = image2.view(dtype=np.uint8).reshape((ydim, xdim, 4)) # Copy the RGBA image into view, flipping it so it comes right-side up # with a lower-left origin - view2[:, :, :] = np.flipud(np.asarray(img_pillow2)) + view2[:, :, :] = np.asarray(img_pillow2) p = figure( - plot_width=910, - plot_height=int(910 / xdim * ydim) + 5, + width=910, + height=int(910 / xdim * ydim) + 5, output_backend=self.output_backend.value, ) @@ -346,7 +346,7 @@ def __init__( view = self.image.view(dtype=np.uint8).reshape((self.ydim, self.xdim, 4)) # Copy the RGBA image into view, flipping it so it comes right-side up # with a lower-left origin - view[:, :, :] = np.flipud(np.asarray(img_pillow)) + view[:, :, :] = np.asarray(img_pillow) # Display the 32-bit RGBA image self.dim = max(self.xdim, self.ydim) @@ -556,7 +556,7 @@ def make_fig(self): fig.image_rgba( image=[self.image], x=0, - y=self.xdim, + y=0, dw=self.ydim, dh=self.xdim, global_alpha=self.tissue_alpha.value, @@ -682,11 +682,11 @@ def add_dea(self): view2 = image2.view(dtype=np.uint8).reshape((ydim, xdim, 4)) # Copy the RGBA image into view, flipping it so it comes right-side up # with a lower-left origin - view2[:, :, :] = np.flipud(np.asarray(img_pillow2)) + view2[:, :, :] = np.asarray(img_pillow2) p = figure( - plot_width=910, - plot_height=int(910 / xdim * ydim) + 5, + width=910, + height=int(910 / xdim * ydim) + 5, output_backend=self.output_backend.value, ) @@ -796,7 +796,7 @@ def __init__( view = self.image.view(dtype=np.uint8).reshape((self.ydim, self.xdim, 4)) # Copy the RGBA image into view, flipping it so it comes right-side up # with a lower-left origin - view[:, :, :] = np.flipud(np.asarray(img_pillow)) + view[:, :, :] = np.asarray(img_pillow) # Display the 32-bit RGBA image self.dim = max(self.xdim, self.ydim) @@ -883,7 +883,7 @@ def make_fig(self): fig.image_rgba( image=[self.image], x=0, - y=self.xdim, + y=0, dw=self.ydim, dh=self.xdim, global_alpha=self.tissue_alpha.value, @@ -975,7 +975,7 @@ def __init__( view = self.image.view(dtype=np.uint8).reshape((self.ydim, self.xdim, 4)) # Copy the RGBA image into view, flipping it so it comes right-side up # with a lower-left origin - view[:, :, :] = np.flipud(np.asarray(img_pillow)) + view[:, :, :] = np.asarray(img_pillow) # Display the 32-bit RGBA image self.dim = max(self.xdim, self.ydim) @@ -1072,7 +1072,7 @@ def make_fig(self): fig.image_rgba( image=[self.image], x=0, - y=self.xdim, + y=0, dw=self.ydim, dh=self.xdim, global_alpha=self.tissue_alpha.value, @@ -1264,7 +1264,7 @@ def __init__( view = self.image.view(dtype=np.uint8).reshape((self.ydim, self.xdim, 4)) # Copy the RGBA image into view, flipping it so it comes right-side up # with a lower-left origin - view[:, :, :] = np.flipud(np.asarray(img_pillow)) + view[:, :, :] = np.asarray(img_pillow) # Display the 32-bit RGBA image self.dim = max(self.xdim, self.ydim) @@ -1320,7 +1320,7 @@ def make_fig(self): fig.image_rgba( image=[self.image], x=0, - y=self.xdim, + y=0, dw=self.ydim, dh=self.xdim, global_alpha=self.tissue_alpha.value, diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index e4d1be19..0cd6645a 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -30,7 +30,7 @@ def cluster_plot( cmap: Optional[str] = "default", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index 1352cc6e..7df51516 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -35,7 +35,7 @@ def feat_plot( cmap: Optional[str] = "Spectral_r", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index cd074aa1..c755d12b 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -33,7 +33,7 @@ def gene_plot( cmap: Optional[str] = "Spectral_r", use_label: Optional[str] = None, list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes._subplots.Axes] = None, + ax: Optional[matplotlib.axes.Axes] = None, fig: Optional[matplotlib.figure.Figure] = None, show_plot: Optional[bool] = True, show_axis: Optional[bool] = False, diff --git a/stlearn/utils.py b/stlearn/utils.py index 97180035..0ea54262 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -26,7 +26,7 @@ class Empty(Enum): from abc import ABC -class _AxesSubplot(Axes, axes.SubplotBase, ABC): +class _AxesSubplot(Axes, axes.SubplotBase): """Intersection between Axes and SubplotBase: Has methods of both""" diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 3bbcac4d..89a96f43 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -193,6 +193,10 @@ def Read10X( adata.obs["imagerow"] = image_coor[:, 1] adata.uns["spatial"][library_id]["use_quality"] = quality + adata.obs["array_row"] = adata.obs["array_row"].astype(int) + adata.obs["array_col"] = adata.obs["array_col"].astype(int) + adata.obsm["spatial"] = adata.obsm["spatial"].astype("int64") + return adata From 4d9f05245aac8ecdb2514f744c2c668a054de991 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 23:28:17 +0000 Subject: [PATCH 009/241] Bump flask from 2.0.3 to 2.3.2 in /stlearn/app Bumps [flask](https://github.com/pallets/flask) from 2.0.3 to 2.3.2. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/2.0.3...2.3.2) --- updated-dependencies: - dependency-name: flask dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- stlearn/app/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/app/requirements.txt b/stlearn/app/requirements.txt index 4bf914e3..e6aa1aea 100644 --- a/stlearn/app/requirements.txt +++ b/stlearn/app/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.0.3 +Flask==2.3.2 flask_wtf==1.0.0 markupsafe==2.1.0 WTForms==3.0.1 From 09c8d7a79979268fe78273fcb25726c5e94c2c6c Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Wed, 24 May 2023 00:16:15 +1000 Subject: [PATCH 010/241] feature: shortest path finding --- stlearn/spatials/trajectory/__init__.py | 1 + stlearn/spatials/trajectory/pseudotime.py | 3 +- .../trajectory/shortest_path_spatial_PAGA.py | 94 +++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py diff --git a/stlearn/spatials/trajectory/__init__.py b/stlearn/spatials/trajectory/__init__.py index 6140a09d..0a1dc6c7 100644 --- a/stlearn/spatials/trajectory/__init__.py +++ b/stlearn/spatials/trajectory/__init__.py @@ -11,3 +11,4 @@ from .compare_transitions import compare_transitions from .set_root import set_root +from .shortest_path_spatial_PAGA import shortest_path_spatial_PAGA \ No newline at end of file diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index ba056a78..f0f674b1 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -244,9 +244,8 @@ def store_available_paths(adata, threshold, use_label, max_nodes, pseudotime_key paths = nx.all_simple_paths(H, source=source, target=target) for i, path in enumerate(paths): if len(path) < max_nodes: - all_paths[i] = path + all_paths[str(i) + "_" + str(source) + "_" + str(target)] = path - # all_paths = list(map(lambda x: " - ".join(np.array(x).astype(str)),all_paths)) adata.uns["available_paths"] = all_paths print( diff --git a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py b/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py new file mode 100644 index 00000000..6340d27d --- /dev/null +++ b/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py @@ -0,0 +1,94 @@ +import networkx as nx +import numpy as np +from stlearn.utils import _read_graph + +def shortest_path_spatial_PAGA(adata,use_label,key="dpt_pseudotime",): + # Read original PAGA graph + G = nx.from_numpy_array(adata.uns["paga"]["connectivities"].toarray()) + edge_weights = nx.get_edge_attributes(G, "weight") + G.remove_edges_from((e for e, w in edge_weights.items() if w <0)) + H = G.to_directed() + + # Get min_node and max_node + min_node,max_node = find_min_max_node(adata,key,use_label) + + # Calculate pseudotime for each node + node_pseudotime = {} + + for node in H.nodes: + node_pseudotime[node] = adata.obs.query(use_label + " == '" + str(node) + "'")[ + key + ].max() + + # Force original PAGA to directed PAGA based on pseudotime + edge_to_remove = [] + for edge in H.edges: + if node_pseudotime[edge[0]] - node_pseudotime[edge[1]] > 0: + edge_to_remove.append(edge) + H.remove_edges_from(edge_to_remove) + + # Extract all available paths + all_paths = {} + j = 0 + for source in H.nodes: + for target in H.nodes: + paths = nx.all_simple_paths(H, source=source, target=target) + for i, path in enumerate(paths): + j+=1 + all_paths[j] = path + + # Filter the target paths from min_node to max_node + target_paths = [] + for path in list(all_paths.values()): + if path[0] == min_node and path[-1] == max_node: + target_paths.append(path) + + # Get the global graph + G = _read_graph(adata, "global_graph") + + centroid_dict = adata.uns["centroid_dict"] + centroid_dict = {int(key): centroid_dict[key] for key in centroid_dict} + + # Generate total length of every path. Store by dictionary + dist_dict = {} + for path in target_paths: + path_name = ",".join(list(map(str,path))) + result = [] + query_node = get_node(path, adata.uns["split_node"]) + for edge in G.edges(): + if (edge[0] in query_node) and (edge[1] in query_node): + result.append(edge) + if len(result) >= len(path): + dist_dict[path_name] = calculate_total_dist(result,centroid_dict) + + # Find the shortest path + shortest_path = min(dist_dict, key=lambda x: dist_dict[x]) + return shortest_path.split(',') + +# get name of cluster by subcluster +def get_cluster(search, dictionary): + for cl, sub in dictionary.items(): + if search in sub: + return cl + +def get_node(node_list, split_node): + result = np.array([]) + for node in node_list: + result = np.append(result, np.array(split_node[int(node)]).astype(int)) + return result.astype(int) + +def find_min_max_node(adata,key="dpt_pseudotime",use_label="leiden"): + min_cluster = int(adata.obs[adata.obs[key]==0][use_label].values[0]) + max_cluster = int(adata.obs[adata.obs[key]==1][use_label].values[0]) + + return [min_cluster,max_cluster] + +def calculate_total_dist(result,centroid_dict): + import math + total_dist = 0 + for edge in result: + source = centroid_dict[edge[0]] + target = centroid_dict[edge[1]] + dist =math.dist(source,target) + total_dist += dist + return total_dist \ No newline at end of file From 3fe707a9b50f7a6ea15e80844609446aef446eb3 Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Sat, 28 Oct 2023 12:14:27 +0100 Subject: [PATCH 011/241] fix: python 3.10 type error with None in edge weight --- stlearn/spatials/trajectory/__init__.py | 2 +- stlearn/spatials/trajectory/global_level.py | 10 ++-- stlearn/spatials/trajectory/pseudotime.py | 1 - .../trajectory/shortest_path_spatial_PAGA.py | 56 +++++++++++-------- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/stlearn/spatials/trajectory/__init__.py b/stlearn/spatials/trajectory/__init__.py index 0a1dc6c7..bd6c4820 100644 --- a/stlearn/spatials/trajectory/__init__.py +++ b/stlearn/spatials/trajectory/__init__.py @@ -11,4 +11,4 @@ from .compare_transitions import compare_transitions from .set_root import set_root -from .shortest_path_spatial_PAGA import shortest_path_spatial_PAGA \ No newline at end of file +from .shortest_path_spatial_PAGA import shortest_path_spatial_PAGA diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatials/trajectory/global_level.py index 60f39658..6a898238 100644 --- a/stlearn/spatials/trajectory/global_level.py +++ b/stlearn/spatials/trajectory/global_level.py @@ -19,7 +19,6 @@ def global_level( verbose: bool = True, copy: bool = False, ) -> Optional[AnnData]: - """\ Perform global sptial trajectory inference. @@ -152,7 +151,6 @@ def global_level( labels = nx.get_edge_attributes(H_sub, "weight") for edge, _ in labels.items(): - dm = dm_list[order_big_dict[query_dict[edge[0]]]] sdm = sdm_list[order_big_dict[query_dict[edge[0]]]] @@ -160,7 +158,11 @@ def global_level( order_dict[edge[0]], order_dict[edge[1]] ] * (1 - w) H_sub[edge[0]][edge[1]]["weight"] = weight - # tmp = H_sub + + # Set edges with weight=None to weight=0 + for u, v, tmp in H_sub.edges(data=True): + if tmp.get("weight") is None: + H_sub[u][v]["weight"] = 0 H_sub = nx.algorithms.tree.minimum_spanning_arborescence(H_sub) H_nodes = list(range(len(H_sub.nodes))) @@ -236,7 +238,6 @@ def ordering_nodes(node_list, use_label, adata): def spatial_distance_matrix(adata, cluster1, cluster2, use_label): - tmp = adata.obs[adata.obs[use_label] == str(cluster1)] chosen_adata1 = adata[list(tmp.index)] tmp = adata.obs[adata.obs[use_label] == str(cluster2)] @@ -267,7 +268,6 @@ def spatial_distance_matrix(adata, cluster1, cluster2, use_label): def ge_distance_matrix(adata, cluster1, cluster2, use_label, use_rep, n_dims): - tmp = adata.obs[adata.obs[use_label] == str(cluster1)] chosen_adata1 = adata[list(tmp.index)] tmp = adata.obs[adata.obs[use_label] == str(cluster2)] diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index f0f674b1..0c9df496 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -246,7 +246,6 @@ def store_available_paths(adata, threshold, use_label, max_nodes, pseudotime_key if len(path) < max_nodes: all_paths[str(i) + "_" + str(source) + "_" + str(target)] = path - adata.uns["available_paths"] = all_paths print( "All available trajectory paths are stored in adata.uns['available_paths'] with length < " diff --git a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py b/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py index 6340d27d..bfd6b359 100644 --- a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py +++ b/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py @@ -2,16 +2,21 @@ import numpy as np from stlearn.utils import _read_graph -def shortest_path_spatial_PAGA(adata,use_label,key="dpt_pseudotime",): + +def shortest_path_spatial_PAGA( + adata, + use_label, + key="dpt_pseudotime", +): # Read original PAGA graph G = nx.from_numpy_array(adata.uns["paga"]["connectivities"].toarray()) edge_weights = nx.get_edge_attributes(G, "weight") - G.remove_edges_from((e for e, w in edge_weights.items() if w <0)) + G.remove_edges_from((e for e, w in edge_weights.items() if w < 0)) H = G.to_directed() - + # Get min_node and max_node - min_node,max_node = find_min_max_node(adata,key,use_label) - + min_node, max_node = find_min_max_node(adata, key, use_label) + # Calculate pseudotime for each node node_pseudotime = {} @@ -26,7 +31,7 @@ def shortest_path_spatial_PAGA(adata,use_label,key="dpt_pseudotime",): if node_pseudotime[edge[0]] - node_pseudotime[edge[1]] > 0: edge_to_remove.append(edge) H.remove_edges_from(edge_to_remove) - + # Extract all available paths all_paths = {} j = 0 @@ -34,36 +39,37 @@ def shortest_path_spatial_PAGA(adata,use_label,key="dpt_pseudotime",): for target in H.nodes: paths = nx.all_simple_paths(H, source=source, target=target) for i, path in enumerate(paths): - j+=1 + j += 1 all_paths[j] = path - + # Filter the target paths from min_node to max_node target_paths = [] for path in list(all_paths.values()): if path[0] == min_node and path[-1] == max_node: target_paths.append(path) - + # Get the global graph G = _read_graph(adata, "global_graph") - + centroid_dict = adata.uns["centroid_dict"] centroid_dict = {int(key): centroid_dict[key] for key in centroid_dict} - + # Generate total length of every path. Store by dictionary dist_dict = {} for path in target_paths: - path_name = ",".join(list(map(str,path))) + path_name = ",".join(list(map(str, path))) result = [] query_node = get_node(path, adata.uns["split_node"]) for edge in G.edges(): if (edge[0] in query_node) and (edge[1] in query_node): result.append(edge) if len(result) >= len(path): - dist_dict[path_name] = calculate_total_dist(result,centroid_dict) - + dist_dict[path_name] = calculate_total_dist(result, centroid_dict) + # Find the shortest path shortest_path = min(dist_dict, key=lambda x: dist_dict[x]) - return shortest_path.split(',') + return shortest_path.split(",") + # get name of cluster by subcluster def get_cluster(search, dictionary): @@ -71,24 +77,28 @@ def get_cluster(search, dictionary): if search in sub: return cl + def get_node(node_list, split_node): result = np.array([]) for node in node_list: result = np.append(result, np.array(split_node[int(node)]).astype(int)) return result.astype(int) -def find_min_max_node(adata,key="dpt_pseudotime",use_label="leiden"): - min_cluster = int(adata.obs[adata.obs[key]==0][use_label].values[0]) - max_cluster = int(adata.obs[adata.obs[key]==1][use_label].values[0]) - - return [min_cluster,max_cluster] -def calculate_total_dist(result,centroid_dict): +def find_min_max_node(adata, key="dpt_pseudotime", use_label="leiden"): + min_cluster = int(adata.obs[adata.obs[key] == 0][use_label].values[0]) + max_cluster = int(adata.obs[adata.obs[key] == 1][use_label].values[0]) + + return [min_cluster, max_cluster] + + +def calculate_total_dist(result, centroid_dict): import math + total_dist = 0 for edge in result: source = centroid_dict[edge[0]] target = centroid_dict[edge[1]] - dist =math.dist(source,target) + dist = math.dist(source, target) total_dist += dist - return total_dist \ No newline at end of file + return total_dist From 3284c93207e74ff9cb6f642db76c63bebfddfd66 Mon Sep 17 00:00:00 2001 From: Duy Pham Date: Fri, 1 Dec 2023 10:08:04 +0000 Subject: [PATCH 012/241] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a4c1727..d0593da2 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,6 @@ If you have used stLearn in your research, please consider citing us: -> Pham _et al._, (2020). stLearn: integrating spatial location, tissue morphology and gene expression to find cell types, cell-cell interactions and spatial trajectories within undissociated tissues -> _biorxiv_ -> https://doi.org/10.1101/2020.05.31.125658 +> Pham, Duy, et al. "Robust mapping of spatiotemporal trajectories and cell–cell interactions in healthy and diseased tissues." +> Nature Communications 14.1 (2023): 7739. +> [https://doi.org/10.1101/2020.05.31.125658](https://doi.org/10.1038/s41467-023-43120-6) From 637c1d13528839ad300deb20dd2b3ae6c38eefea Mon Sep 17 00:00:00 2001 From: Duy Pham Date: Sat, 9 Dec 2023 23:00:40 +0000 Subject: [PATCH 013/241] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d0593da2..2a1c47ca 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Conda downloads - Install + Install @@ -39,7 +39,7 @@ Paper - DOI From 1e12664dc19225d69a2332e6585eae437bdac2f1 Mon Sep 17 00:00:00 2001 From: Duy Pham Date: Thu, 14 Dec 2023 18:17:38 +0000 Subject: [PATCH 014/241] Update views.py --- stlearn/app/source/forms/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stlearn/app/source/forms/views.py b/stlearn/app/source/forms/views.py index 420981a9..551c737e 100644 --- a/stlearn/app/source/forms/views.py +++ b/stlearn/app/source/forms/views.py @@ -280,11 +280,11 @@ def run_psts(request, adata, step_log): element_values = list(step_log["psts_params"].values()) if element_values[4] == "Auto": - w = None + model = "mixed" elif element_values[4] == "Spatial distance only": - w = 0 + model = "spatial" else: - w = 1 + model = "gene_expression" if not form.validate_on_submit(): flash_errors(form) @@ -318,7 +318,7 @@ def run_psts(request, adata, step_log): ) print(node_order) st.spatial.trajectory.pseudotimespace_global( - adata, use_label="clusters", list_clusters=node_order, w=w + adata, use_label="clusters", list_clusters=node_order, model=model ) st.pl.cluster_plot( From 79700e0a0d80817c7653408960b3f8e71049940e Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Thu, 28 Dec 2023 17:09:00 +0000 Subject: [PATCH 015/241] fix: fix current issues --- README.md | 2 +- stlearn/adds/annotation.py | 10 +- stlearn/image_preprocessing/image_tiling.py | 4 +- stlearn/plotting/trajectory/__init__.py | 1 + stlearn/plotting/trajectory/tree_plot.py | 2 - .../plotting/trajectory/tree_plot_simple.py | 216 ++++++++++++++++++ 6 files changed, 225 insertions(+), 10 deletions(-) create mode 100644 stlearn/plotting/trajectory/tree_plot_simple.py diff --git a/README.md b/README.md index 2a1c47ca..8f318d6c 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,6 @@ If you have used stLearn in your research, please consider citing us: -> Pham, Duy, et al. "Robust mapping of spatiotemporal trajectories and cell–cell interactions in healthy and diseased tissues." +> Pham, Duy, et al. "Robust mapping of spatiotemporal trajectories and cell–cell interactions in healthy and diseased tissues." > Nature Communications 14.1 (2023): 7739. > [https://doi.org/10.1101/2020.05.31.125658](https://doi.org/10.1038/s41467-023-43120-6) diff --git a/stlearn/adds/annotation.py b/stlearn/adds/annotation.py index ca509382..a8bc1ac9 100644 --- a/stlearn/adds/annotation.py +++ b/stlearn/adds/annotation.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import Optional, Union, List from anndata import AnnData from matplotlib import pyplot as plt from pathlib import Path @@ -7,11 +7,10 @@ def annotation( adata: AnnData, - label_list: list, + label_list: List[str], use_label: str = "louvain", copy: bool = False, ) -> Optional[AnnData]: - """\ Adding annotation for cluster @@ -38,8 +37,9 @@ def annotation( if len(label_list) != len(adata.obs[use_label].unique()): raise ValueError("Please give the correct number of label list!") - adata.obs[use_label + "_anno"] = adata.obs[use_label] - adata.obs[use_label + "_anno"].cat.categories = label_list + adata.obs[use_label + "_anno"] = adata.obs[use_label].cat.rename_categories( + label_list + ) print("The annotation is added to adata.obs['" + use_label + "_anno" + "']") diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index e0cd6712..155e5461 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -13,7 +13,7 @@ def tiling( adata: AnnData, out_path: Union[Path, str] = "./tiling", - library_id: str = None, + library_id: Union[str, None] = None, crop_size: int = 40, target_size: int = 299, verbose: bool = False, @@ -78,7 +78,7 @@ def tiling( (imagecol_left, imagerow_down, imagecol_right, imagerow_up) ) tile.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) - tile.resize((target_size, target_size)) + tile = tile.resize((target_size, target_size)) tile_name = str(imagecol) + "-" + str(imagerow) + "-" + str(crop_size) out_tile = Path(out_path) / (tile_name + ".jpeg") tile_names.append(str(out_tile)) diff --git a/stlearn/plotting/trajectory/__init__.py b/stlearn/plotting/trajectory/__init__.py index 6638c77c..16681a51 100644 --- a/stlearn/plotting/trajectory/__init__.py +++ b/stlearn/plotting/trajectory/__init__.py @@ -1,5 +1,6 @@ from .pseudotime_plot import pseudotime_plot from .local_plot import local_plot +from .tree_plot_simple import tree_plot_simple from .tree_plot import tree_plot from .transition_markers_plot import transition_markers_plot from .DE_transition_plot import DE_transition_plot diff --git a/stlearn/plotting/trajectory/tree_plot.py b/stlearn/plotting/trajectory/tree_plot.py index 3fd33bae..90ade45f 100644 --- a/stlearn/plotting/trajectory/tree_plot.py +++ b/stlearn/plotting/trajectory/tree_plot.py @@ -33,7 +33,6 @@ def tree_plot( ncols: int = 4, copy: bool = False, ) -> Optional[AnnData]: - """\ Hierarchical tree plot represent for the global spatial trajectory inference. @@ -114,7 +113,6 @@ def tree_plot( def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5): - """ From Joel's answer at https://stackoverflow.com/a/29597209/2966723. Licensed under Creative Commons Attribution-Share Alike diff --git a/stlearn/plotting/trajectory/tree_plot_simple.py b/stlearn/plotting/trajectory/tree_plot_simple.py new file mode 100644 index 00000000..3b2395fd --- /dev/null +++ b/stlearn/plotting/trajectory/tree_plot_simple.py @@ -0,0 +1,216 @@ +from matplotlib import pyplot as plt +from PIL import Image +import pandas as pd +import matplotlib +import numpy as np +import networkx as nx +import math +import random +from stlearn._compat import Literal +from typing import Optional, Union +from anndata import AnnData +import warnings +import io +from copy import deepcopy +from stlearn.utils import _read_graph + + +def tree_plot_simple( + adata: AnnData, + library_id: str = None, + figsize: Union[float, int] = (10, 4), + data_alpha: float = 1.0, + use_label: str = "louvain", + spot_size: Union[float, int] = 50, + fontsize: int = 6, + piesize: float = 0.15, + zoom: float = 0.1, + name: str = None, + output: str = None, + dpi: int = 180, + show_all: bool = False, + show_plot: bool = True, + ncols: int = 4, + copy: bool = False, +) -> Optional[AnnData]: + """\ + Hierarchical tree plot represent for the global spatial trajectory inference. + + Parameters + ---------- + adata + Annotated data matrix. + library_id + Library id stored in AnnData. + use_label + Use label result of cluster method. + figsize + Change figure size. + data_alpha + Opacity of the spot. + fontsize + Choose font size. + piesize + Choose the size of cropped image. + zoom + Choose zoom factor. + show_all + Show all cropped image or not. + show_legend + Show legend or not. + dpi + Set dpi as the resolution for the plot. + copy + Return a copy instead of writing to adata. + Returns + ------- + Nothing + """ + + G = _read_graph(adata, "PTS_graph") + + if library_id is None: + library_id = list(adata.uns["spatial"].keys())[0] + + G.remove_node(9999) + + start_nodes = [] + disconnected_nodes = [] + for node in G.in_degree(): + if node[1] == 0: + start_nodes.append(node[0]) + + for node in G.out_degree(): + if node[1] == 0: + disconnected_nodes.append(node[0]) + + start_nodes = list(set(start_nodes) - set(disconnected_nodes)) + start_nodes.sort() + + nrows = math.ceil(len(start_nodes) / ncols) + + superfig, axs = plt.subplots(nrows, ncols, figsize=figsize) + axs = axs.ravel() + + for idx in range(0, nrows * ncols): + try: + generate_tree_viz( + adata, use_label, G, axs[idx], starter_node=start_nodes[idx] + ) + except: + axs[idx] = axs[idx].axis("off") + + if name is None: + name = use_label + + if output is not None: + superfig.savefig( + output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0 + ) + + if show_plot == True: + plt.show() + + +def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5): + """ + From Joel's answer at https://stackoverflow.com/a/29597209/2966723. + Licensed under Creative Commons Attribution-Share Alike + + If the graph is a tree this will return the positions to plot this in a + hierarchical layout. + + G: the graph (must be a tree) + + root: the root node of current branch + - if the tree is directed and this is not given, + the root will be found and used + - if the tree is directed and this is given, then + the positions will be just for the descendants of this node. + - if the tree is undirected and not given, + then a random choice will be used. + + width: horizontal space allocated for this branch - avoids overlap with other branches + + vert_gap: gap between levels of hierarchy + + vert_loc: vertical location of root + + xcenter: horizontal location of root + """ + if not nx.is_tree(G): + raise TypeError("cannot use hierarchy_pos on a graph that is not a tree") + + if root is None: + if isinstance(G, nx.DiGraph): + root = next( + iter(nx.topological_sort(G)) + ) # allows back compatibility with nx version 1.11 + else: + root = random.choice(list(G.nodes)) + + def _hierarchy_pos( + G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, parent=None + ): + """ + see hierarchy_pos docstring for most arguments + + pos: a dict saying where all nodes go if they have been assigned + parent: parent of this branch. - only affects it if non-directed + + """ + + if pos is None: + pos = {root: (xcenter, vert_loc)} + else: + pos[root] = (xcenter, vert_loc) + children = list(G.neighbors(root)) + if not isinstance(G, nx.DiGraph) and parent is not None: + children.remove(parent) + if len(children) != 0: + dx = width / len(children) + nextx = xcenter - width / 2 - dx / 2 + for child in children: + nextx += dx + pos = _hierarchy_pos( + G, + child, + width=dx, + vert_gap=vert_gap, + vert_loc=vert_loc - vert_gap, + xcenter=nextx, + pos=pos, + parent=root, + ) + return pos + + return _hierarchy_pos(G, root, width, vert_gap, vert_loc, xcenter) + + +def generate_tree_viz(adata, use_label, G, axis, starter_node): + tmp_edges = [] + for edge in G.edges(): + if starter_node == edge[0]: + tmp_edges.append(edge) + tmp_D = nx.DiGraph() + tmp_D.add_edges_from(tmp_edges) + + pos = hierarchy_pos(tmp_D) + a = axis + + a.axis("off") + colors = [] + for n in tmp_D: + subset = adata.obs[adata.obs["sub_cluster_labels"] == str(n)] + colors.append(adata.uns[use_label + "_colors"][int(subset[use_label][0])]) + + nx.draw_networkx_edges( + tmp_D, + pos, + ax=a, + arrowstyle="-", + edge_color="#ADABAF", + connectionstyle="angle3,angleA=0,angleB=90", + ) + nx.draw_networkx_nodes(tmp_D, pos, node_color=colors, ax=a) + nx.draw_networkx_labels(tmp_D, pos, font_color="black", ax=a) From e46f73886cb1ddcf2e5dcfd0a7550c70fdbf55ab Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Thu, 28 Dec 2023 17:41:10 +0000 Subject: [PATCH 016/241] feat: allow png in tiling --- requirements.txt | 1 + stlearn/image_preprocessing/image_tiling.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index a3c68fc3..d669bbec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ click>=8.0.4 leidenalg louvain numpy>=1.18,<1.22 +numba<=0.57.1 Pillow>=9.0.1 scanpy>=1.8.2 scikit-image>=0.19.2 diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index 155e5461..bdb88a60 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -16,6 +16,7 @@ def tiling( library_id: Union[str, None] = None, crop_size: int = 40, target_size: int = 299, + img_fmt: str = "JPEG", verbose: bool = False, copy: bool = False, ) -> Optional[AnnData]: @@ -80,15 +81,22 @@ def tiling( tile.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) tile = tile.resize((target_size, target_size)) tile_name = str(imagecol) + "-" + str(imagerow) + "-" + str(crop_size) - out_tile = Path(out_path) / (tile_name + ".jpeg") - tile_names.append(str(out_tile)) + + if img_fmt == "JPEG": + out_tile = Path(out_path) / (tile_name + ".jpeg") + tile_names.append(str(out_tile)) + tile.save(out_tile, "JPEG") + else: + out_tile = Path(out_path) / (tile_name + ".png") + tile_names.append(str(out_tile)) + tile.save(out_tile, "PNG") + if verbose: print( "generate tile at location ({}, {})".format( str(imagecol), str(imagerow) ) ) - tile.save(out_tile, "JPEG") pbar.update(1) From 913b70a59736ef860635bb1e880cec72d4914085 Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Thu, 28 Dec 2023 17:42:04 +0000 Subject: [PATCH 017/241] fix: pre-commit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d669bbec..c5452f88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ bokeh>= 2.4.2 click>=8.0.4 leidenalg louvain -numpy>=1.18,<1.22 numba<=0.57.1 +numpy>=1.18,<1.22 Pillow>=9.0.1 scanpy>=1.8.2 scikit-image>=0.19.2 From 1c8b40c0f656f777083f6aaf9c6d0e3de6037e44 Mon Sep 17 00:00:00 2001 From: duypham2108 Date: Tue, 19 Mar 2024 01:07:08 +0000 Subject: [PATCH 018/241] fix: fix several bugs --- stlearn/image_preprocessing/segmentation.py | 2 +- stlearn/plotting/cci_plot_helpers.py | 19 +++++++++++++++---- stlearn/plotting/classes.py | 2 +- stlearn/tools/microenv/cci/base.py | 7 +++---- stlearn/tools/microenv/cci/het.py | 2 +- stlearn/tools/microenv/cci/het_helpers.py | 4 ++-- stlearn/tools/microenv/cci/permutation.py | 4 ++-- stlearn/wrapper/read.py | 7 +------ 8 files changed, 26 insertions(+), 21 deletions(-) diff --git a/stlearn/image_preprocessing/segmentation.py b/stlearn/image_preprocessing/segmentation.py index 2cc0d642..76023058 100644 --- a/stlearn/image_preprocessing/segmentation.py +++ b/stlearn/image_preprocessing/segmentation.py @@ -158,7 +158,7 @@ def _calculate_morph_stats(tile_path): min_nucleus_area = 60 im_nuclei_seg_mask = htk.segmentation.label.area_open( labels, min_nucleus_area - ).astype(np.int) + ).astype(np.int64) # compute nuclei properties objProps = skimage.measure.regionprops(im_nuclei_seg_mask) diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index a4554ecd..045612e0 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -198,9 +198,11 @@ def rank_scatter( y[ranks_], alpha=alpha, c=highlight_color, - s=None - if type(point_sizes) == type(None) - else (point_sizes[ranks_] ** point_size_exp), + s=( + None + if type(point_sizes) == type(None) + else (point_sizes[ranks_] ** point_size_exp) + ), edgecolors=color, ) ranks = ranks_ if not show_all else ranks @@ -754,7 +756,16 @@ def chordDiagram(X, ax, colors=None, width=0.1, pad=2, chordwidth=0.7, lim=1.1): # This draws the outter ring # # IdeogramArc(start=start, end=end, radius=1.0, ax=ax, # color=colors[i], width=width) - a = Arc((0, 0), diam, diam, 0, start, end, color=colors[i], lw=10) + a = Arc( + xy=(0, 0), + width=diam, + height=diam, + angle=0, + theta1=start, + theta2=end, + color=colors[i], + lw=10, + ) ax.add_patch(a) start, end = pos[(i, i)] # This draws the paths to itself # diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index f2c4a073..e60c7a0e 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -194,7 +194,7 @@ def _crop_image(self, main_ax: _AxesSubplot, margin: float): def _zoom_image(self, main_ax: _AxesSubplot, zoom_coord: Optional[float]): main_ax.set_xlim(zoom_coord[0], zoom_coord[1]) - main_ax.set_ylim(zoom_coord[2], zoom_coord[3]) + main_ax.set_ylim(zoom_coord[3], zoom_coord[2]) def _add_color_bar(self, plot, color_bar_label: str = ""): cb = plt.colorbar( diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tools/microenv/cci/base.py index 1b0afb82..ecea7ecc 100644 --- a/stlearn/tools/microenv/cci/base.py +++ b/stlearn/tools/microenv/cci/base.py @@ -16,7 +16,6 @@ def lr( neighbours: list = None, fast: bool = True, ) -> AnnData: - """Calculate the proportion of known ligand-receptor co-expression among the neighbouring spots or within spots Parameters ---------- @@ -111,7 +110,7 @@ def get_lrs_scores( lrs: np.array lr pairs from the database in format ['L1_R1', 'LN_RN'] """ if type(spot_indices) == type(None): - spot_indices = np.array(list(range(len(adata))), dtype=np.int_) + spot_indices = np.array(list(range(len(adata))), dtype=np.int32) spot_lr1s = get_spot_lrs( adata, lr_pairs=lrs, lr_order=True, filter_pairs=filter_pairs @@ -208,7 +207,7 @@ def calc_neighbours( distance, ) if index: - n_index = np.array(n_index, dtype=np.int_) + n_index = np.array(n_index, dtype=np.int32) neighbours.append(n_index[n_index != i]) else: n_spots = adata.obs_names[n_index] @@ -286,6 +285,7 @@ def lr_pandas( ------- lr_scores: numpy.ndarray Cells*LR-scores. """ + # function to calculate mean of lr2 expression between neighbours or within spot (distance==0) for each spot def mean_lr2(x): # get lr2 expressions from the neighbour(s) @@ -352,7 +352,6 @@ def lr_grid( radius: int = 1, verbose: bool = True, ) -> AnnData: - """Calculate the proportion of known ligand-receptor co-expression among the neighbouring grids or within each grid Parameters ---------- diff --git a/stlearn/tools/microenv/cci/het.py b/stlearn/tools/microenv/cci/het.py index e7d3cffe..bc6fb221 100644 --- a/stlearn/tools/microenv/cci/het.py +++ b/stlearn/tools/microenv/cci/het.py @@ -348,7 +348,7 @@ def get_interactions( A_gene1_sig_bool = np.logical_and(A_gene1_bool, sig_bool) n_true = A_gene1_sig_bool.sum() - A_gene1_sig_indices = np.zeros((1, n_true), dtype=np.int_)[ + A_gene1_sig_indices = np.zeros((1, n_true), dtype=np.int32)[ 0, : ] # np.where(A_gene1_sig_bool)[0] index = 0 diff --git a/stlearn/tools/microenv/cci/het_helpers.py b/stlearn/tools/microenv/cci/het_helpers.py index 8faa2464..270e811c 100644 --- a/stlearn/tools/microenv/cci/het_helpers.py +++ b/stlearn/tools/microenv/cci/het_helpers.py @@ -218,7 +218,7 @@ def get_data_for_counting(adata, use_label, mix_mode, all_set): cell_data = np.zeros((len(cell_labels), len(all_set)), dtype=np.float64) for i, cell_type in enumerate(all_set): cell_data[:, i] = ( - (cell_labels == cell_type).astype(np.int_).astype(np.float64) + (cell_labels == cell_type).astype(np.int32).astype(np.float64) ) spot_bcs = adata.obs_names.values.astype(str) @@ -254,7 +254,7 @@ def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): cell_data = np.zeros((len(cell_labels), len(all_set)), dtype=np.float64) for i, cell_type in enumerate(all_set): cell_data[:, i] = ( - (cell_labels == cell_type).astype(np.int_).astype(np.float64) + (cell_labels == cell_type).astype(np.int32).astype(np.float64) ) spot_bcs = adata.obs_names.values.astype(str) diff --git a/stlearn/tools/microenv/cci/permutation.py b/stlearn/tools/microenv/cci/permutation.py index 42cda543..6ca6ce12 100644 --- a/stlearn/tools/microenv/cci/permutation.py +++ b/stlearn/tools/microenv/cci/permutation.py @@ -13,6 +13,7 @@ from .merge import merge from .perm_utils import get_lr_features, get_lr_bg + # Newest method # def perform_spot_testing( adata: AnnData, @@ -74,7 +75,7 @@ def perform_spot_testing( ######## Background per LR, but only for spots where LR has a score ######## # Determine the indices of the spots where each LR has a score # cols = ["n_spots", "n_spots_sig", "n_spots_sig_pval"] - lr_summary = np.zeros((lr_scores.shape[1], 3), np.int) + lr_summary = np.zeros((lr_scores.shape[1], 3), np.int32) pvals = np.ones(lr_scores.shape, dtype=np.float64) pvals_adj = np.ones(lr_scores.shape, dtype=np.float64) log10pvals_adj = np.zeros(lr_scores.shape, dtype=np.float64) @@ -349,7 +350,6 @@ def permutation( background: np.array = None, **kwargs, ) -> AnnData: - """Permutation test for merged result Parameters ---------- diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 89a96f43..a66bf512 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -1,5 +1,6 @@ """Reading and Writing """ + from pathlib import Path, PurePath from typing import Optional, Union from anndata import AnnData @@ -27,7 +28,6 @@ def Read10X( quality: _QUALITY = "hires", image_path: Union[str, Path] = None, ) -> AnnData: - """\ Read Visium data from 10X (wrap read_visium from scanpy) @@ -209,7 +209,6 @@ def ReadOldST( quality: str = "hires", spot_diameter_fullres: float = 50, ) -> AnnData: - """\ Read Old Spatial Transcriptomics data @@ -258,7 +257,6 @@ def ReadSlideSeq( spot_diameter_fullres: float = 50, background_color: _background = "white", ) -> AnnData: - """\ Read Slide-seq data @@ -340,7 +338,6 @@ def ReadMERFISH( spot_diameter_fullres: float = 50, background_color: _background = "white", ) -> AnnData: - """\ Read MERFISH data @@ -423,7 +420,6 @@ def ReadSeqFish( spot_diameter_fullres: float = 50, background_color: _background = "white", ) -> AnnData: - """\ Read SeqFish data @@ -510,7 +506,6 @@ def ReadXenium( spot_diameter_fullres: float = 15, background_color: _background = "white", ) -> AnnData: - """\ Read Xenium data From f540d55b780d095e28c9c0754e50a4273303def8 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 3 Jun 2025 09:53:15 +1000 Subject: [PATCH 019/241] Update source with black code formatter. --- stlearn/adds/add_deconvolution.py | 1 - stlearn/adds/add_image.py | 1 - stlearn/adds/add_loupe_clusters.py | 1 - stlearn/adds/add_mask.py | 4 ++-- stlearn/adds/add_positions.py | 1 - stlearn/adds/parsing.py | 1 - stlearn/app/source/forms/form_validators.py | 4 ++-- stlearn/app/source/forms/forms.py | 6 +++--- stlearn/app/source/forms/view_helpers.py | 3 +-- stlearn/embedding/fa.py | 1 - stlearn/embedding/ica.py | 1 - stlearn/logging.py | 4 ++-- stlearn/plotting/cci_plot_helpers.py | 5 ++--- stlearn/plotting/classes_bokeh.py | 8 ++++---- stlearn/plotting/cluster_plot.py | 1 - stlearn/plotting/deconvolution_plot.py | 1 - stlearn/plotting/feat_plot.py | 1 + stlearn/plotting/non_spatial_plot.py | 1 - stlearn/plotting/stack_3d_plot.py | 1 - .../plotting/trajectory/DE_transition_plot.py | 17 ++++++++--------- stlearn/plotting/trajectory/local_plot.py | 1 - stlearn/plotting/trajectory/pseudotime_plot.py | 1 - .../trajectory/transition_markers_plot.py | 8 ++++---- stlearn/spatials/clustering/localization.py | 1 - stlearn/spatials/trajectory/local_level.py | 1 - stlearn/spatials/trajectory/pseudotime.py | 1 - stlearn/spatials/trajectory/pseudotimespace.py | 2 -- stlearn/spatials/trajectory/set_root.py | 1 - stlearn/spatials/trajectory/utils.py | 6 ++---- stlearn/tools/clustering/kmeans.py | 1 - stlearn/tools/microenv/cci/analysis.py | 5 +++-- stlearn/tools/microenv/cci/base_grouping.py | 4 ++-- stlearn/tools/microenv/cci/go.py | 3 +-- stlearn/wrapper/read.py | 3 +-- tests/utils.py | 6 +++--- 35 files changed, 41 insertions(+), 66 deletions(-) diff --git a/stlearn/adds/add_deconvolution.py b/stlearn/adds/add_deconvolution.py index 3b5445be..6f571395 100644 --- a/stlearn/adds/add_deconvolution.py +++ b/stlearn/adds/add_deconvolution.py @@ -10,7 +10,6 @@ def add_deconvolution( annotation_path: Union[Path, str], copy: bool = False, ) -> Optional[AnnData]: - """\ Adding label transfered from Seurat diff --git a/stlearn/adds/add_image.py b/stlearn/adds/add_image.py index 83c92d6b..39f895fd 100644 --- a/stlearn/adds/add_image.py +++ b/stlearn/adds/add_image.py @@ -18,7 +18,6 @@ def image( spot_diameter_fullres: float = 50, copy: bool = False, ) -> Optional[AnnData]: - """\ Adding image data to the Anndata object diff --git a/stlearn/adds/add_loupe_clusters.py b/stlearn/adds/add_loupe_clusters.py index af614bd8..116d8eec 100644 --- a/stlearn/adds/add_loupe_clusters.py +++ b/stlearn/adds/add_loupe_clusters.py @@ -13,7 +13,6 @@ def add_loupe_clusters( key_add: str = "multiplex", copy: bool = False, ) -> Optional[AnnData]: - """\ Adding label transfered from Seurat diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index 6608e00f..515fa1e9 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -168,8 +168,8 @@ def apply_mask( """ ) mask_image_2d = mask_image.mean(axis=2) - apply_spot_mask = ( - lambda x: [i, mask] + apply_spot_mask = lambda x: ( + [i, mask] if mask_image_2d[int(x["imagerow"]), int(x["imagecol"])] == 1 else [x[key + "_code"], x[key]] ) diff --git a/stlearn/adds/add_positions.py b/stlearn/adds/add_positions.py index 52872384..3435c32d 100644 --- a/stlearn/adds/add_positions.py +++ b/stlearn/adds/add_positions.py @@ -12,7 +12,6 @@ def positions( quality: str = "low", copy: bool = False, ) -> Optional[AnnData]: - """\ Adding spatial information into the Anndata object diff --git a/stlearn/adds/parsing.py b/stlearn/adds/parsing.py index d92932cc..db241dcd 100644 --- a/stlearn/adds/parsing.py +++ b/stlearn/adds/parsing.py @@ -12,7 +12,6 @@ def parsing( coordinates_file: Union[Path, str], copy: bool = True, ) -> Optional[AnnData]: - """\ Parsing the old spaital transcriptomics data diff --git a/stlearn/app/source/forms/form_validators.py b/stlearn/app/source/forms/form_validators.py index 3a82f887..5afc6d3c 100644 --- a/stlearn/app/source/forms/form_validators.py +++ b/stlearn/app/source/forms/form_validators.py @@ -1,5 +1,5 @@ -""" Contains different kinds of form validators. -""" +"""Contains different kinds of form validators.""" + from wtforms.validators import ValidationError diff --git a/stlearn/app/source/forms/forms.py b/stlearn/app/source/forms/forms.py index 0eef6b1d..53ae1908 100644 --- a/stlearn/app/source/forms/forms.py +++ b/stlearn/app/source/forms/forms.py @@ -1,7 +1,7 @@ """Purpose of this script is to create general forms that are programmable with - particular input. Will impliment forms for subsetting the data and - visualisation options in a general way so can be used with any - SingleCellAnalysis dataset. +particular input. Will impliment forms for subsetting the data and +visualisation options in a general way so can be used with any +SingleCellAnalysis dataset. """ import sys diff --git a/stlearn/app/source/forms/view_helpers.py b/stlearn/app/source/forms/view_helpers.py index 499edd7e..ac9c4ea4 100644 --- a/stlearn/app/source/forms/view_helpers.py +++ b/stlearn/app/source/forms/view_helpers.py @@ -1,5 +1,4 @@ -""" Helper functions for views.py. -""" +"""Helper functions for views.py.""" import numpy diff --git a/stlearn/embedding/fa.py b/stlearn/embedding/fa.py index 9c463aee..715de4d5 100644 --- a/stlearn/embedding/fa.py +++ b/stlearn/embedding/fa.py @@ -18,7 +18,6 @@ def run_fa( use_data: str = None, copy: bool = False, ) -> Optional[AnnData]: - """\ Factor Analysis (FA) A simple linear generative model with Gaussian latent variables. diff --git a/stlearn/embedding/ica.py b/stlearn/embedding/ica.py index ed2f1d21..a42cda5a 100644 --- a/stlearn/embedding/ica.py +++ b/stlearn/embedding/ica.py @@ -14,7 +14,6 @@ def run_ica( use_data: str = None, copy: bool = False, ) -> Optional[AnnData]: - """\ FastICA: a fast algorithm for Independent Component Analysis. diff --git a/stlearn/logging.py b/stlearn/logging.py index 674e7f77..63c0f6eb 100644 --- a/stlearn/logging.py +++ b/stlearn/logging.py @@ -1,5 +1,5 @@ -"""Logging and Profiling -""" +"""Logging and Profiling""" + import logging from functools import update_wrapper, partial from logging import CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index 045612e0..6753e44b 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -1,5 +1,4 @@ -""" Helper functions for cci_plot.py. -""" +"""Helper functions for cci_plot.py.""" import sys import math @@ -159,7 +158,7 @@ def rank_scatter( y, alpha=alpha, c=color, - s=None if type(point_sizes) == type(None) else point_sizes ** point_size_exp, + s=None if type(point_sizes) == type(None) else point_sizes**point_size_exp, edgecolors="none", ) y_min, y_max = ax.get_ylim() diff --git a/stlearn/plotting/classes_bokeh.py b/stlearn/plotting/classes_bokeh.py index 7a75b2df..484d495b 100644 --- a/stlearn/plotting/classes_bokeh.py +++ b/stlearn/plotting/classes_bokeh.py @@ -548,7 +548,7 @@ def make_fig(self): title="Cluster plot", x_range=(0, self.dim - 150), y_range=(self.dim, 0), - output_backend=self.output_backend.value + output_backend=self.output_backend.value, # Specifying xdim/ydim isn't quire right :-( # width=xdim, height=ydim, ) @@ -1410,9 +1410,9 @@ def change_click(): empty_array[:] = np.NaN empty_array = empty_array.astype(object) for i in range(0, len(self.adata[0].uns["annotation"])): - empty_array[ - [np.array(self.adata[0].uns["annotation"]["spot"][i])] - ] = str(self.adata[0].uns["annotation"]["label"][i]) + empty_array[[np.array(self.adata[0].uns["annotation"]["spot"][i])]] = ( + str(self.adata[0].uns["annotation"]["label"][i]) + ) empty_array = pd.Series(empty_array).fillna("other") self.adata[0].obs["annotation"] = pd.Categorical(empty_array) diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index 0cd6645a..a212a317 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -61,7 +61,6 @@ def cluster_plot( trajectory_edge_color: Optional[str] = "#f4efd3", trajectory_arrowsize: Optional[int] = 17, ) -> Optional[AnnData]: - """\ Allows the visualization of a cluster results as the discretes values of dot points in the Spatial transcriptomics array. We also support to diff --git a/stlearn/plotting/deconvolution_plot.py b/stlearn/plotting/deconvolution_plot.py index e69c2b13..96f83caa 100644 --- a/stlearn/plotting/deconvolution_plot.py +++ b/stlearn/plotting/deconvolution_plot.py @@ -33,7 +33,6 @@ def deconvolution_plot( figsize: tuple = (6.4, 4.8), show=True, ) -> Optional[AnnData]: - """\ Clustering plot for sptial transcriptomics data. Also it has a function to display trajectory inference. diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index 7df51516..3f3b272a 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -23,6 +23,7 @@ from bokeh.io import push_notebook, output_notebook from bokeh.plotting import show + # @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) def feat_plot( adata: AnnData, diff --git a/stlearn/plotting/non_spatial_plot.py b/stlearn/plotting/non_spatial_plot.py index 4e6447d0..e7992d63 100644 --- a/stlearn/plotting/non_spatial_plot.py +++ b/stlearn/plotting/non_spatial_plot.py @@ -17,7 +17,6 @@ def non_spatial_plot( adata: AnnData, use_label: str = "louvain", ) -> Optional[AnnData]: - """\ A wrap function to plot all the non-spatial plot from scanpy. diff --git a/stlearn/plotting/stack_3d_plot.py b/stlearn/plotting/stack_3d_plot.py index 4128c97f..f229672a 100644 --- a/stlearn/plotting/stack_3d_plot.py +++ b/stlearn/plotting/stack_3d_plot.py @@ -11,7 +11,6 @@ def stack_3d_plot( use_label=None, gene_symbol=None, ) -> Optional[AnnData]: - """\ Clustering plot for sptial transcriptomics data. Also it has a function to display trajectory inference. diff --git a/stlearn/plotting/trajectory/DE_transition_plot.py b/stlearn/plotting/trajectory/DE_transition_plot.py index 2fc82ebd..d988099c 100644 --- a/stlearn/plotting/trajectory/DE_transition_plot.py +++ b/stlearn/plotting/trajectory/DE_transition_plot.py @@ -12,7 +12,6 @@ def DE_transition_plot( dpi: int = 150, output: str = None, ) -> Optional[AnnData]: - """\ Differential expression between transition markers. @@ -136,7 +135,7 @@ def DE_transition_plot( rect.get_y() + rect.get_height() / 2.0, gene_name, **alignment, - size=font_size + size=font_size, ) axes[0][1].text( rect.get_x() + 0.01, @@ -144,7 +143,7 @@ def DE_transition_plot( p_value, color="w", **alignment, - size=font_size + size=font_size, ) rects = axes[0][0].patches @@ -161,7 +160,7 @@ def DE_transition_plot( rect.get_y() + rect.get_height() / 2.0, gene_name, **alignment, - size=font_size + size=font_size, ) axes[0][0].text( rect.get_x() - 0.01, @@ -169,7 +168,7 @@ def DE_transition_plot( p_value, color="w", **alignment, - size=font_size + size=font_size, ) rects = axes[1][1].patches @@ -186,7 +185,7 @@ def DE_transition_plot( rect.get_y() + rect.get_height() / 2.0, gene_name, **alignment, - size=font_size + size=font_size, ) axes[1][1].text( rect.get_x() + 0.01, @@ -194,7 +193,7 @@ def DE_transition_plot( p_value, color="w", **alignment, - size=font_size + size=font_size, ) rects = axes[1][0].patches @@ -211,7 +210,7 @@ def DE_transition_plot( rect.get_y() + rect.get_height() / 2.0, gene_name, **alignment, - size=font_size + size=font_size, ) axes[1][0].text( rect.get_x() - 0.01, @@ -219,7 +218,7 @@ def DE_transition_plot( p_value, color="w", **alignment, - size=font_size + size=font_size, ) plt.figtext( diff --git a/stlearn/plotting/trajectory/local_plot.py b/stlearn/plotting/trajectory/local_plot.py index 878d1666..a520f8a9 100644 --- a/stlearn/plotting/trajectory/local_plot.py +++ b/stlearn/plotting/trajectory/local_plot.py @@ -32,7 +32,6 @@ def local_plot( output: str = None, copy: bool = False, ) -> Optional[AnnData]: - """\ Local spatial trajectory inference plot. diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index 54359c5a..14c67030 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -39,7 +39,6 @@ def pseudotime_plot( copy: bool = False, ax=None, ) -> Optional[AnnData]: - """\ Global trajectory inference plot (Only DPT). diff --git a/stlearn/plotting/trajectory/transition_markers_plot.py b/stlearn/plotting/trajectory/transition_markers_plot.py index 9f81d100..b68ec7fe 100644 --- a/stlearn/plotting/trajectory/transition_markers_plot.py +++ b/stlearn/plotting/trajectory/transition_markers_plot.py @@ -100,7 +100,7 @@ def transition_markers_plot( rect.get_y() + rect.get_height() / 2.0, gene_name, **alignment, - size=6 + size=6, ) axes[1].text( rect.get_x() + 0.01, @@ -108,7 +108,7 @@ def transition_markers_plot( p_value, color="w", **alignment, - size=6 + size=6, ) rects = axes[0].patches @@ -125,7 +125,7 @@ def transition_markers_plot( rect.get_y() + rect.get_height() / 2.0, gene_name, **alignment, - size=6 + size=6, ) axes[0].text( rect.get_x() - 0.01, @@ -133,7 +133,7 @@ def transition_markers_plot( p_value, color="w", **alignment, - size=6 + size=6, ) plt.figtext(0.5, 0.9, trajectory, ha="center", va="center") diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index c91dd9c7..58f83f8f 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -13,7 +13,6 @@ def localization( min_samples: int = 0, copy: bool = False, ) -> Optional[AnnData]: - """\ Perform local cluster by using DBSCAN. diff --git a/stlearn/spatials/trajectory/local_level.py b/stlearn/spatials/trajectory/local_level.py index c68888f9..79c0b6e4 100644 --- a/stlearn/spatials/trajectory/local_level.py +++ b/stlearn/spatials/trajectory/local_level.py @@ -15,7 +15,6 @@ def local_level( verbose: bool = True, copy: bool = False, ) -> Optional[AnnData]: - """\ Perform local sptial trajectory inference (required run pseudotime first). diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index 0c9df496..3710ee61 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -24,7 +24,6 @@ def pseudotime( run_knn: bool = False, copy: bool = False, ) -> Optional[AnnData]: - """\ Perform pseudotime analysis. diff --git a/stlearn/spatials/trajectory/pseudotimespace.py b/stlearn/spatials/trajectory/pseudotimespace.py index 230d0ff0..d238a428 100644 --- a/stlearn/spatials/trajectory/pseudotimespace.py +++ b/stlearn/spatials/trajectory/pseudotimespace.py @@ -15,7 +15,6 @@ def pseudotimespace_global( step=0.01, k=10, ) -> Optional[AnnData]: - """\ Perform pseudo-time-space analysis with global level. @@ -68,7 +67,6 @@ def pseudotimespace_local( cluster: list = [], w: float = None, ) -> Optional[AnnData]: - """\ Perform pseudo-time-space analysis with local level. diff --git a/stlearn/spatials/trajectory/set_root.py b/stlearn/spatials/trajectory/set_root.py index b26c7909..0805c0c6 100644 --- a/stlearn/spatials/trajectory/set_root.py +++ b/stlearn/spatials/trajectory/set_root.py @@ -5,7 +5,6 @@ def set_root(adata: AnnData, use_label: str, cluster: str, use_raw: bool = False): - """\ Automatically set the root index. diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatials/trajectory/utils.py index 54ea41be..e7ab2909 100644 --- a/stlearn/spatials/trajectory/utils.py +++ b/stlearn/spatials/trajectory/utils.py @@ -581,9 +581,7 @@ def _mat_mat_corr_sparse( n = X.shape[1] X_bar = np.reshape(np.array(X.mean(axis=1)), (-1, 1)) - X_std = np.reshape( - np.sqrt(np.array(X.power(2).mean(axis=1)) - (X_bar**2)), (-1, 1) - ) + X_std = np.reshape(np.sqrt(np.array(X.power(2).mean(axis=1)) - (X_bar**2)), (-1, 1)) y_bar = np.reshape(np.mean(Y, axis=0), (1, -1)) y_std = np.reshape(np.std(Y, axis=0), (1, -1)) @@ -629,7 +627,7 @@ def _correlation_test_helper( """ def perm_test_extractor( - res: Sequence[Tuple[np.ndarray, np.ndarray]] + res: Sequence[Tuple[np.ndarray, np.ndarray]], ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: pvals, corr_bs = zip(*res) pvals = np.sum(pvals, axis=0) / float(n_perms) diff --git a/stlearn/tools/clustering/kmeans.py b/stlearn/tools/clustering/kmeans.py index 0b451cb1..43d4d6da 100644 --- a/stlearn/tools/clustering/kmeans.py +++ b/stlearn/tools/clustering/kmeans.py @@ -20,7 +20,6 @@ def kmeans( key_added: str = "kmeans", copy: bool = False, ) -> Optional[AnnData]: - """\ Perform kmeans cluster for spatial transcriptomics data diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index adbb2d19..d6421a64 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -1,5 +1,5 @@ -""" Wrapper function for performing CCI analysis, varrying the analysis based on - the inputted data / state of the anndata object. +"""Wrapper function for performing CCI analysis, varrying the analysis based on +the inputted data / state of the anndata object. """ import os @@ -24,6 +24,7 @@ ) from statsmodels.stats.multitest import multipletests + ################################################################################ # Functions related to Ligand-Receptor interactions # ################################################################################ diff --git a/stlearn/tools/microenv/cci/base_grouping.py b/stlearn/tools/microenv/cci/base_grouping.py index a24b21e8..882bb011 100644 --- a/stlearn/tools/microenv/cci/base_grouping.py +++ b/stlearn/tools/microenv/cci/base_grouping.py @@ -1,5 +1,5 @@ -""" Performs LR analysis by grouping LR pairs which having hotspots across - similar tissues. +"""Performs LR analysis by grouping LR pairs which having hotspots across +similar tissues. """ from stlearn.pl import het_plot diff --git a/stlearn/tools/microenv/cci/go.py b/stlearn/tools/microenv/cci/go.py index 617a7fe5..eff77d09 100644 --- a/stlearn/tools/microenv/cci/go.py +++ b/stlearn/tools/microenv/cci/go.py @@ -1,5 +1,4 @@ -""" Wrapper for performing the LR GO analysis. -""" +"""Wrapper for performing the LR GO analysis.""" import os import stlearn.tools.microenv.cci.r_helpers as rhs diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index a66bf512..42f2d2eb 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -1,5 +1,4 @@ -"""Reading and Writing -""" +"""Reading and Writing""" from pathlib import Path, PurePath from typing import Optional, Union diff --git a/tests/utils.py b/tests/utils.py index a10b5d21..6a5cc78f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,7 +10,7 @@ def read_test_data(): path = os.path.dirname(os.path.realpath(__file__)) adata = sc.read_h5ad(f"{path}/test_data/test_data.h5") im = Image.open(f"{path}/test_data/test_image.jpg") - adata.uns["spatial"]["V1_Breast_Cancer_Block_A_Section_1"]["images"][ - "hires" - ] = np.array(im) + adata.uns["spatial"]["V1_Breast_Cancer_Block_A_Section_1"]["images"]["hires"] = ( + np.array(im) + ) return adata From 975261091b752e25fae03ab8f26550c40115c03c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 3 Jun 2025 10:42:50 +1000 Subject: [PATCH 020/241] Fix some quality issues. --- stlearn/__init__.py | 5 ++--- stlearn/utils.py | 13 ++----------- stlearn/wrapper/convert_scanpy.py | 5 +---- stlearn/wrapper/read.py | 16 ++++++++-------- 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 1fc79b20..8b2712ef 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -1,9 +1,8 @@ """Top-level package for stLearn.""" __author__ = """Genomics and Machine Learning lab""" -__email__ = "duy.pham@uq.edu.au" -__version__ = "0.4.11" - +__email__ = "andrew.newman@uq.edu.au" +__version__ = "0.4.2" from . import add from . import pp diff --git a/stlearn/utils.py b/stlearn/utils.py index 0ea54262..ba9e1280 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -1,18 +1,15 @@ import numpy as np -import pandas as pd -import io -from PIL import Image -import matplotlib from anndata import AnnData import networkx as nx from typing import Optional, Union, Mapping # Special -from typing import Sequence, Iterable # ABCs from typing import Tuple # Classes from textwrap import dedent from enum import Enum +from matplotlib import axes +from matplotlib.axes import Axes class Empty(Enum): @@ -21,10 +18,6 @@ class Empty(Enum): _empty = Empty.token -from matplotlib import rcParams, ticker, gridspec, axes -from matplotlib.axes import Axes -from abc import ABC - class _AxesSubplot(Axes, axes.SubplotBase): """Intersection between Axes and SubplotBase: Has methods of both""" @@ -110,7 +103,6 @@ def _check_img( def _check_coords( obsm: Optional[Mapping], scale_factor: Optional[float] ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]: - image_coor = obsm["spatial"] * scale_factor imagecol = image_coor[:, 0] imagerow = image_coor[:, 1] @@ -119,7 +111,6 @@ def _check_coords( def _read_graph(adata: AnnData, graph_type: Optional[str]): - if graph_type == "PTS_graph": graph = nx.from_scipy_sparse_array( adata.uns[graph_type]["graph"], create_using=nx.DiGraph diff --git a/stlearn/wrapper/convert_scanpy.py b/stlearn/wrapper/convert_scanpy.py index 4cf7e288..e384f0a0 100644 --- a/stlearn/wrapper/convert_scanpy.py +++ b/stlearn/wrapper/convert_scanpy.py @@ -1,8 +1,5 @@ -from typing import Optional, Union +from typing import Optional from anndata import AnnData -from matplotlib import pyplot as plt -from pathlib import Path -import os def convert_scanpy( diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 42f2d2eb..d400d816 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -1,6 +1,6 @@ """Reading and Writing""" -from pathlib import Path, PurePath +from pathlib import Path from typing import Optional, Union from anndata import AnnData import numpy as np @@ -9,10 +9,10 @@ import stlearn from .._compat import Literal import scanpy -import scipy import matplotlib.pyplot as plt from matplotlib.image import imread import json +import logging as logg _QUALITY = Literal["fulres", "hires", "lowres"] _background = ["black", "white"] @@ -185,7 +185,7 @@ def Read10X( else: scale = adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] + ] image_coor = adata.obsm["spatial"] * scale adata.obs["imagecol"] = image_coor[:, 0] @@ -318,7 +318,7 @@ def ReadSlideSeq( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" @@ -401,7 +401,7 @@ def ReadMERFISH( adata_merfish.uns["spatial"][library_id]["scalefactors"] = {} adata_merfish.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata_merfish.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -487,7 +487,7 @@ def ReadSeqFish( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -578,7 +578,7 @@ def ReadXenium( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -658,7 +658,7 @@ def create_stlearn( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres From 2df855967ed262a9105478ddca4aaf93898aeb77 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 3 Jun 2025 10:43:11 +1000 Subject: [PATCH 021/241] Update project configuration. --- CONTRIBUTING.rst | 26 +++++++++++++------- pyproject.toml | 55 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 22 +++++++++-------- setup.cfg | 21 ----------------- setup.py | 13 +++++----- tests/test_CCI.py | 25 +++++++++----------- tests/test_install.py | 16 ------------- 7 files changed, 103 insertions(+), 75 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 tests/test_install.py diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index aa232892..fbc922f4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -64,11 +64,19 @@ Ready to contribute? Here's how to set up `stlearn` for local development. $ git clone git@github.com:your_name_here/stlearn.git -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: +3. Install your local copy into a virtualenv. This is how you set up your fork for local development:: - $ mkvirtualenv stlearn + $ conda create -n stlearn-dev python=3.10 + $ conda activate stlearn-dev $ cd stlearn/ - $ python setup.py develop + $ pip install -e .[dev,test] + + Or if you prefer pip/virtualenv:: + + $ python -m venv stlearn-env + $ source stlearn-env/bin/activate # On Windows: stlearn-env\Scripts\activate + $ cd stlearn/ + $ pip install -e .[dev,test] 4. Create a branch for local development:: @@ -76,14 +84,16 @@ Ready to contribute? Here's how to set up `stlearn` for local development. Now you can make your changes locally. -5. When you're done making changes, check that your changes pass flake8 and the - tests, including testing other Python versions with tox:: +5. When you're done making changes, check that your changes pass linters and tests:: + $ black stlearn tests $ flake8 stlearn tests - $ python setup.py test or pytest - $ tox + $ mypy stlearn + $ pytest + +Or run everything with tox:: - To get flake8 and tox, just pip install them into your virtualenv. + $ tox 6. Commit your changes and push your branch to GitHub:: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..5c67efd9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "stlearn" +version = "0.4.2" +authors = [ + {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, +] +description = "A downstream analysis toolkit for Spatial Transcriptomic data" +readme = {file = "README.md", content-type = "text/markdown"} +license = {text = "BSD license"} +requires-python = ">=3.10" +keywords = ["stlearn"] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dynamic = ["dependencies"] + +[project.optional-dependencies] +dev = [ + "black", + "flake8", + "mypy", + "pytest", + "tox", +] +test = [ + "pytest", + "pytest-cov", +] + +[project.urls] +Homepage = "https://github.com/BiomedicalMachineLearning/stLearn" +Repository = "https://github.com/BiomedicalMachineLearning/stLearn" + +[project.scripts] +stlearn = "stlearn.app.cli:main" + +[tool.setuptools.packages.find] +include = ["stlearn", "stlearn.*"] + +[tool.setuptools.package-data] +"*" = ["*"] + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c5452f88..616bcd91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,12 @@ -bokeh>= 2.4.2 -click>=8.0.4 -leidenalg -louvain -numba<=0.57.1 -numpy>=1.18,<1.22 -Pillow>=9.0.1 -scanpy>=1.8.2 -scikit-image>=0.19.2 -tensorflow +bokeh==3.7.3 +click==8.2.1 +leidenalg==0.10.2 +louvain==0.8.2 +numba==0.55.2 +numpy==1.22.4 +pillow==11.2.1 +scanpy==1.9.8 +scikit-image==0.22.0 +tensorflow==2.13.1 +imageio==2.37.0 +scipy==1.11.4 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index f877626d..00000000 --- a/setup.cfg +++ /dev/null @@ -1,21 +0,0 @@ -[bumpversion] -current_version = 0.4.11 -commit = True -tag = True - -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' - -[bumpversion:file:stlearn/__init__.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' - -[bdist_wheel] -universal = 1 - -[flake8] -exclude = docs - -[aliases] -# Define setup.py command aliases here diff --git a/setup.py b/setup.py index e728fba4..292288be 100644 --- a/setup.py +++ b/setup.py @@ -20,16 +20,17 @@ setup( author="Genomics and Machine Learning lab", - author_email="duy.pham@uq.edu.au", - python_requires=">=3.7", + author_email="andrew.newman@uq.edu.au", + python_requires=">=3.10", classifiers=[ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], description="A downstream analysis toolkit for Spatial Transcriptomic data", entry_points={ @@ -49,6 +50,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/BiomedicalMachineLearning/stLearn", - version="0.4.11", + version="0.4.2", zip_safe=False, ) diff --git a/tests/test_CCI.py b/tests/test_CCI.py index b17af940..e31e887f 100644 --- a/tests/test_CCI.py +++ b/tests/test_CCI.py @@ -2,15 +2,12 @@ """Tests for `stlearn` package.""" -import os - import unittest import numpy as np from numba.typed import List import stlearn as st -import scanpy as sc from tests.utils import read_test_data import stlearn.tools.microenv.cci.het_helpers as het_hs @@ -26,7 +23,7 @@ class TestCCI(unittest.TestCase): def setUp(self) -> None: """Setup some basic test-cases as sanity checks.""" - ##### Unit neighbourhood, containing just 1 spot and 6 neighbours ###### + # Unit neighbourhood, containing just 1 spot and 6 neighbours """ * A is the middle spot, B/C/D/E/F/G are the neighbouring spots clock- wise starting at the top-left. @@ -55,7 +52,7 @@ def setUp(self) -> None: self.neighbourhood_indices = neighbourhood_indices self.neigh_dict = neigh_dict - ##### Basic tests ####### + # Basic tests def test_load_lrs(self): """Testing loading lr database.""" sizes = [2293, 4071] # lit lr db size, putative lr db size. @@ -71,7 +68,7 @@ def test_load_lrs(self): lrs = st.tl.cci.load_lrs() self.assertEqual(len(lrs), sizes[0]) - ### Testing loading mouse as species #### + # Testing loading mouse as species lrs = st.tl.cci.load_lrs(species="mouse") genes1 = [lr_.split("_")[0] for lr_ in lrs] genes2 = [lr_.split("_")[1] for lr_ in lrs] @@ -80,9 +77,9 @@ def test_load_lrs(self): self.assertTrue(np.all([gene[0].isupper() for gene in genes2])) self.assertTrue(np.all([gene[1:] == gene[1:].lower() for gene in genes2])) - ####### Important, granular tests related to LR scoring ######### + # Important, granular tests related to LR scoring - ###### Important, granular tests related to CCI counting ####### + # Important, granular tests related to CCI counting def test_edge_retrieval_basic(self): """ Basic test of functionality to retrieve edges via \ get_between_spot_edge_array. @@ -93,7 +90,7 @@ def test_edge_retrieval_basic(self): # Initialising the edge list # edge_list = het_hs.init_edge_list(neighbourhood_bcs) - ############# Basic case, should populate with all edges ############### + # Basic case, should populate with all edges neigh_bool = np.array([True] * len(neighbourhood_bcs)) cell_data = np.array([1] * len(neighbourhood_bcs), dtype=np.float64) het_hs.get_between_spot_edge_array( @@ -115,7 +112,7 @@ def test_edge_retrieval_basic(self): np.all([edge in all_edges or edge[::-1] in all_edges for edge in edge_list]) ) - ########### Some neighbours not valid but no effect on edge list ####### + # Some neighbours not valid but no effect on edge list # No effect since though not a valid neighbour, still a valid spot # edge_list = het_hs.init_edge_list(neighbourhood_bcs) invalid_neighs = ["B", "E"] @@ -130,7 +127,7 @@ def test_edge_retrieval_basic(self): np.all([edge in all_edges or edge[::-1] in all_edges for edge in edge_list]) ) - ########### Some neighbours not valid, effects the edge list ########### + # Some neighbours not valid, effects the edge list # Two neighbouring spots no longer valid neighbours # edge_list = het_hs.init_edge_list(neighbourhood_bcs) invalid_neighs = ["B", "C"] @@ -152,7 +149,7 @@ def test_edge_retrieval_basic(self): ) ) - ######### Middle spot not neighbour, cell type, or spot of interest #### + # Middle spot not neighbour, cell type, or spot of interest # Removing the centre-spot as being the cell type of interest # neigh_bool = np.array([True] * len(neighbourhood_bcs)) neigh_bool[0] = False @@ -177,7 +174,7 @@ def test_edge_retrieval_basic(self): ) ) - ### Corner spot valid neighbour, not cell type, not spot of interest ### + # Corner spot valid neighbour, not cell type, not spot of interest neigh_bool = np.array([True] * len(neighbourhood_bcs)) cell_data = np.array([1] * len(neighbourhood_bcs), dtype=np.float64) cell_data[1] = 0 @@ -209,7 +206,7 @@ def test_get_interactions(self): and spots of another cell type expressing the receptor. """ - ####### Case 1 ###### + # Case 1 """ Middle spot only spot of interest. Cell type 1, 2, or 3. Middle spot expresses ligand. diff --git a/tests/test_install.py b/tests/test_install.py deleted file mode 100644 index 5ff4160e..00000000 --- a/tests/test_install.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Tests that everything is installed correctly. -""" - -import unittest - - -class TestCCI(unittest.TestCase): - """Tests for `stlearn` importability, i.e. correct installation.""" - - def test_SME(self): - import stlearn.spatials.SME.normalize as sme_normalise - - def test_cci(self): - """Tests CCI can be imported.""" - import stlearn.tools.microenv.cci.analysis as an From 34af76bbb7da685634ebab874a5dacff5c31a6bc Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 3 Jun 2025 10:48:18 +1000 Subject: [PATCH 022/241] Improve list of files to ignore. --- .gitignore | 63 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index c5ab06d4..b5495d15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,68 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class *.pyc -.ipynb_checkpoints -*/.ipynb_checkpoints/* + +# C extensions +*.so + +# Distribution / packaging +.Python build/ +data/samples +develop-eggs/ dist/ -*.egg-info +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Unit tests / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Jupyter Notebook +.ipynb_checkpoints +*/.ipynb_checkpoints/* /*.ipynb + +# Data files /*.csv -output/ + +# MacOS caching .DS_Store */.DS_Store + +# PyCharm etc .idea/ + +# Sphinx documentation docs/_build + +# Distribution/package/temporary files data/ tiling/ -.pytest_cache figures/ *.h5ad -inferCNV/ -stlearn/tools/microenv/cci/junk_code.py -stlearn/tools/microenv/cci/.Rhistory From 7b762c4f28049b76754661e39f31beae62ed9af6 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 3 Jun 2025 10:48:29 +1000 Subject: [PATCH 023/241] Small reformat. --- stlearn/wrapper/read.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index d400d816..3f6a1f52 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -185,7 +185,7 @@ def Read10X( else: scale = adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] + ] image_coor = adata.obsm["spatial"] * scale adata.obs["imagecol"] = image_coor[:, 0] @@ -318,7 +318,7 @@ def ReadSlideSeq( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" @@ -401,7 +401,7 @@ def ReadMERFISH( adata_merfish.uns["spatial"][library_id]["scalefactors"] = {} adata_merfish.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata_merfish.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -487,7 +487,7 @@ def ReadSeqFish( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -578,7 +578,7 @@ def ReadXenium( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -658,7 +658,7 @@ def create_stlearn( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres From c301830d8fadeefa04ae0feb8223566b5afe1b4a Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 3 Jun 2025 12:00:32 +1000 Subject: [PATCH 024/241] More style issues. --- .readthedocs.yml | 2 +- stlearn/__init__.py | 19 +++++++++++++++++++ stlearn/wrapper/read.py | 30 ++++++++++++++++++------------ tox.ini | 36 ++++++++++++++++++++++++++---------- 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 6a8f1a14..3ee3a06a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,4 +1,4 @@ build: image: latest python: - version: 3.8 + version: 3.10 diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 8b2712ef..d3a734d5 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -27,3 +27,22 @@ from .wrapper.concatenate_spatial_adata import concatenate_spatial_adata # from . import cli +__all__ = [ + "add", + "pp", + "em", + "tl", + "pl", + "spatial", + "datasets", + "ReadSlideSeq", + "Read10X", + "ReadOldST", + "ReadMERFISH", + "ReadSeqFish", + "ReadXenium", + "create_stlearn", + "settings", + "convert_scanpy", + "concatenate_spatial_adata", +] \ No newline at end of file diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 3f6a1f52..9db3dc25 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -372,7 +372,7 @@ def ReadMERFISH( adata_merfish = counts[coordinates.index, :] adata_merfish.obsm["spatial"] = coordinates.to_numpy() - if scale == None: + if scale is None: max_coor = np.max(adata_merfish.obsm["spatial"]) scale = 2000 / max_coor @@ -429,11 +429,13 @@ def ReadSeqFish( spatial_file Path to spatial location file. library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. + Identifier for the visium library. Can be modified when concatenating multiple + adata objects. scale Set scale factor. quality - Set quality that convert to stlearn to use. Store in anndata.obs['imagecol' & 'imagerow'] + Set quality that convert to stlearn to use. Store in + anndata.obs['imagecol' & 'imagerow'] field Set field of view for SeqFish data spot_diameter_fullres @@ -457,7 +459,7 @@ def ReadSeqFish( adata = AnnData(count) - if scale == None: + if scale is None: max_coor = np.max(spatial[["X", "Y"]]) scale = 2000 / max_coor @@ -517,11 +519,13 @@ def ReadXenium( image_path Path to image. Only need when loading full resolution image. library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. + Identifier for the visium library. Can be modified when concatenating multiple + adata objects. scale Set scale factor. quality - Set quality that convert to stlearn to use. Store in anndata.obs['imagecol' & 'imagerow'] + Set quality that convert to stlearn to use. Store in + anndata.obs['imagecol' & 'imagerow'] spot_diameter_fullres Diameter of spot in full resolution background_color @@ -540,7 +544,7 @@ def ReadXenium( adata.obsm["spatial"] = spatial.values - if scale == None: + if scale is None: max_coor = np.max(adata.obsm["spatial"]) scale = 2000 / max_coor @@ -550,7 +554,7 @@ def ReadXenium( adata.obs["imagecol"] = spatial["imagecol"].values * scale adata.obs["imagerow"] = spatial["imagerow"].values * scale - if image_path != None: + if image_path is not None: stlearn.add.image( adata, library_id=library_id, @@ -606,11 +610,13 @@ def create_stlearn( spatial Pandas Dataframe of spatial location of cells/spots. library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. + Identifier for the visium library. Can be modified when concatenating multiple + adata objects. scale Set scale factor. quality - Set quality that convert to stlearn to use. Store in anndata.obs['imagecol' & 'imagerow'] + Set quality that convert to stlearn to use. Store in + anndata.obs['imagecol' & 'imagerow'] spot_diameter_fullres Diameter of spot in full resolution background_color @@ -623,14 +629,14 @@ def create_stlearn( adata.obsm["spatial"] = spatial.values - if scale == None: + if scale is None: max_coor = np.max(adata.obsm["spatial"]) scale = 2000 / max_coor adata.obs["imagecol"] = spatial["imagecol"].values * scale adata.obs["imagerow"] = spatial["imagerow"].values * scale - if image_path != None: + if image_path is not None: stlearn.add.image( adata, library_id=library_id, diff --git a/tox.ini b/tox.ini index 9aae612f..473ed981 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,36 @@ [tox] -envlist = py35, py36, py37, py38, flake8 +requires = + tox>=4 +env_list = lint, type, 3.1{3,2,1,0}, flake8 -[travis] -python = - 3.8: py38 - 3.7: py37 - 3.6: py36 - 3.5: py35 +[testenv:lint] +description = run linters +skip_install = true +deps = + black +commands = black {posargs:.} + +[testenv:type] +description = run type checks +deps = + mypy +commands = + mypy {posargs:stlearn tests} [testenv:flake8] -basepython = python +description = run flake8 linting +skip_install = true deps = flake8 -commands = flake8 stlearn +commands = flake8 stlearn tests [testenv] setenv = PYTHONPATH = {toxinidir} +deps = + pytest +commands = pytest {posargs} -commands = python setup.py test +[flake8] +max-line-length = 88 +extend-ignore = E203, W503 +exclude = .git,__pycache__,build,dist \ No newline at end of file From b4cfc6b000fea4de0cd6ba60a4d8370b693cd7bc Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 3 Jun 2025 16:39:55 +1000 Subject: [PATCH 025/241] Fix style issues. --- TODO.md | 7 + pyproject.toml | 17 +- stlearn/__init__.py | 34 +- stlearn/__main__.py | 1 - stlearn/_compat.py | 2 +- stlearn/_datasets/_datasets.py | 7 +- stlearn/_settings.py | 117 ++-- stlearn/add.py | 10 - stlearn/adds/add_deconvolution.py | 11 +- stlearn/adds/add_image.py | 16 +- stlearn/adds/add_labels.py | 44 +- stlearn/adds/add_loupe_clusters.py | 13 +- stlearn/adds/add_lr.py | 34 +- stlearn/adds/add_mask.py | 34 +- stlearn/adds/add_positions.py | 13 +- stlearn/adds/annotation.py | 9 +- stlearn/adds/parsing.py | 15 +- stlearn/app/app.py | 55 +- stlearn/app/cli.py | 6 +- stlearn/app/source/forms/form_validators.py | 2 +- stlearn/app/source/forms/forms.py | 161 ++--- stlearn/app/source/forms/helper_functions.py | 5 +- stlearn/app/source/forms/utils.py | 2 - stlearn/app/source/forms/view_helpers.py | 1 - stlearn/app/source/forms/views.py | 36 +- stlearn/classes.py | 29 +- stlearn/datasets.py | 1 - stlearn/em.py | 5 - stlearn/embedding/diffmap.py | 9 +- stlearn/embedding/fa.py | 7 +- stlearn/embedding/ica.py | 11 +- stlearn/embedding/pca.py | 24 +- stlearn/embedding/umap.py | 33 +- .../image_preprocessing/feature_extractor.py | 16 +- stlearn/image_preprocessing/image_tiling.py | 19 +- stlearn/image_preprocessing/model_zoo.py | 10 +- stlearn/image_preprocessing/segmentation.py | 11 +- stlearn/logging.py | 14 +- stlearn/pl.py | 24 - stlearn/plotting/QC_plot.py | 6 +- stlearn/plotting/_docs.py | 27 +- stlearn/plotting/cci_plot.py | 576 +++++++++--------- stlearn/plotting/cci_plot_helpers.py | 237 ++++--- stlearn/plotting/classes.py | 574 +++++++++-------- stlearn/plotting/classes_bokeh.py | 133 ++-- stlearn/plotting/cluster_plot.py | 105 ++-- stlearn/plotting/deconvolution_plot.py | 68 +-- stlearn/plotting/feat_plot.py | 78 +-- stlearn/plotting/gene_plot.py | 83 ++- stlearn/plotting/mask_plot.py | 38 +- stlearn/plotting/non_spatial_plot.py | 17 +- stlearn/plotting/stack_3d_plot.py | 47 +- stlearn/plotting/subcluster_plot.py | 71 +-- .../plotting/trajectory/DE_transition_plot.py | 18 +- stlearn/plotting/trajectory/__init__.py | 20 +- .../plotting/trajectory/check_trajectory.py | 38 +- stlearn/plotting/trajectory/local_plot.py | 55 +- .../plotting/trajectory/pseudotime_plot.py | 88 +-- .../trajectory/transition_markers_plot.py | 20 +- stlearn/plotting/trajectory/tree_plot.py | 89 ++- .../plotting/trajectory/tree_plot_simple.py | 89 ++- stlearn/plotting/trajectory/utils.py | 1 - stlearn/plotting/utils.py | 42 +- stlearn/pp.py | 19 +- stlearn/preprocessing/filter_genes.py | 19 +- stlearn/preprocessing/graph.py | 31 +- stlearn/preprocessing/log_scale.py | 30 +- stlearn/preprocessing/normalize.py | 25 +- stlearn/spatial.py | 14 +- stlearn/spatials/SME/__init__.py | 8 +- stlearn/spatials/SME/_weighting_matrix.py | 20 +- stlearn/spatials/SME/impute.py | 93 +-- stlearn/spatials/SME/normalize.py | 44 +- stlearn/spatials/clustering/__init__.py | 4 + stlearn/spatials/clustering/localization.py | 18 +- stlearn/spatials/morphology/__init__.py | 4 + stlearn/spatials/morphology/adjust.py | 9 +- stlearn/spatials/smooth/__init__.py | 4 + stlearn/spatials/smooth/disk.py | 23 +- stlearn/spatials/trajectory/__init__.py | 32 +- .../trajectory/detect_transition_markers.py | 37 +- stlearn/spatials/trajectory/global_level.py | 81 +-- stlearn/spatials/trajectory/local_level.py | 26 +- stlearn/spatials/trajectory/pseudotime.py | 77 ++- .../spatials/trajectory/pseudotimespace.py | 73 ++- stlearn/spatials/trajectory/set_root.py | 10 +- .../trajectory/shortest_path_spatial_PAGA.py | 1 + stlearn/spatials/trajectory/utils.py | 138 ++--- .../trajectory/weight_optimization.py | 45 +- stlearn/tl.py | 8 +- stlearn/tools/clustering/__init__.py | 8 +- stlearn/tools/clustering/annotate.py | 3 +- stlearn/tools/clustering/kmeans.py | 34 +- stlearn/tools/clustering/louvain.py | 33 +- stlearn/tools/label/label.py | 83 +-- stlearn/tools/microenv/cci/__init__.py | 11 +- stlearn/tools/microenv/cci/analysis.py | 116 ++-- stlearn/tools/microenv/cci/base.py | 282 +++++---- stlearn/tools/microenv/cci/base_grouping.py | 116 ++-- stlearn/tools/microenv/cci/go.py | 3 +- stlearn/tools/microenv/cci/het.py | 53 +- stlearn/tools/microenv/cci/het_helpers.py | 71 +-- stlearn/tools/microenv/cci/merge.py | 12 +- stlearn/tools/microenv/cci/perm_utils.py | 46 +- stlearn/tools/microenv/cci/permutation.py | 256 ++++---- stlearn/utils.py | 39 +- stlearn/wrapper/concatenate_spatial_adata.py | 2 +- stlearn/wrapper/convert_scanpy.py | 4 +- stlearn/wrapper/read.py | 103 ++-- tests/test_CCI.py | 5 +- tests/test_PSTS.py | 6 +- tests/test_SME.py | 4 +- tests/utils.py | 3 +- tox.ini | 17 +- 114 files changed, 2752 insertions(+), 2836 deletions(-) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..8da59d28 --- /dev/null +++ b/TODO.md @@ -0,0 +1,7 @@ +# TODO + +[] > Python 3.8 +[] Fix quality issues +[] Replace tensorflow with Pytorch +[] Upgrade dependencies + [] Numba \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5c67efd9..fda624e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dynamic = ["dependencies"] [project.optional-dependencies] dev = [ "black", - "flake8", + "ruff", "mypy", "pytest", "tox", @@ -52,4 +52,17 @@ include = ["stlearn", "stlearn.*"] "*" = ["*"] [tool.setuptools.dynamic] -dependencies = {file = ["requirements.txt"]} \ No newline at end of file +dependencies = {file = ["requirements.txt"]} + +[tool.ruff] +line-length=88 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "N", "UP"] +ignore = ["E722", "F811", "N802", "N803", "N806", "N818", "N999", "UP031"] +exclude = [".git", "__pycache__", "build", "dist"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" \ No newline at end of file diff --git a/stlearn/__init__.py b/stlearn/__init__.py index d3a734d5..9736f217 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -4,27 +4,21 @@ __email__ = "andrew.newman@uq.edu.au" __version__ = "0.4.2" -from . import add -from . import pp -from . import em -from . import tl -from . import pl -from . import spatial -from . import datasets - -# Wrapper - -from .wrapper.read import ReadSlideSeq -from .wrapper.read import Read10X -from .wrapper.read import ReadOldST -from .wrapper.read import ReadMERFISH -from .wrapper.read import ReadSeqFish -from .wrapper.read import ReadXenium -from .wrapper.read import create_stlearn - +from . import add, datasets, em, pl, pp, spatial, tl from ._settings import settings -from .wrapper.convert_scanpy import convert_scanpy from .wrapper.concatenate_spatial_adata import concatenate_spatial_adata +from .wrapper.convert_scanpy import convert_scanpy + +# Wrapper +from .wrapper.read import ( + Read10X, + ReadMERFISH, + ReadOldST, + ReadSeqFish, + ReadSlideSeq, + ReadXenium, + create_stlearn, +) # from . import cli __all__ = [ @@ -45,4 +39,4 @@ "settings", "convert_scanpy", "concatenate_spatial_adata", -] \ No newline at end of file +] diff --git a/stlearn/__main__.py b/stlearn/__main__.py index 981709a2..4687bf58 100644 --- a/stlearn/__main__.py +++ b/stlearn/__main__.py @@ -5,6 +5,5 @@ from stlearn.app import main - if __name__ == "__main__": # pragma: no cover main() diff --git a/stlearn/_compat.py b/stlearn/_compat.py index 0ef291a2..ba28b435 100644 --- a/stlearn/_compat.py +++ b/stlearn/_compat.py @@ -2,7 +2,7 @@ from typing import Literal except ImportError: try: - from typing_extensions import Literal + from typing import Literal except ImportError: class LiteralMeta(type): diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index 19ffb6d5..a637aed3 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -1,13 +1,14 @@ import scanpy as sc -from .._settings import settings -from pathlib import Path from anndata import AnnData +from .._settings import settings + def example_bcba() -> AnnData: """\ Download processed BCBA data (10X genomics published data). - Reference: https://support.10xgenomics.com/spatial-gene-expression/datasets/1.1.0/V1_Breast_Cancer_Block_A_Section_1 + Reference: + https://support.10xgenomics.com/spatial-gene-expression/datasets/1.1.0/V1_Breast_Cancer_Block_A_Section_1 """ settings.datasetdir.mkdir(exist_ok=True) filename = settings.datasetdir / "example_bcba.h5" diff --git a/stlearn/_settings.py b/stlearn/_settings.py index 30eb017a..91d8b617 100644 --- a/stlearn/_settings.py +++ b/stlearn/_settings.py @@ -1,16 +1,16 @@ import inspect import sys -from contextlib import contextmanager +from collections.abc import Iterable +from contextlib import AbstractContextManager, contextmanager from enum import IntEnum +from logging import getLevelName from pathlib import Path from time import time -from logging import getLevelName -from typing import Any, Union, Optional, Iterable, TextIO -from typing import Tuple, List, ContextManager +from typing import Any, TextIO from . import logging -from .logging import _set_log_level, _set_log_file, _RootLogger from ._compat import Literal +from .logging import _RootLogger, _set_log_file, _set_log_level # All the code here migrated from scanpy # It help to work with scanpy package @@ -40,7 +40,7 @@ def level(self) -> int: return getLevelName(_VERBOSITY_TO_LOGLEVEL[self]) @contextmanager - def override(self, verbosity: "Verbosity") -> ContextManager["Verbosity"]: + def override(self, verbosity: "Verbosity") -> AbstractContextManager["Verbosity"]: """\ Temporarily override verbosity """ @@ -49,7 +49,7 @@ def override(self, verbosity: "Verbosity") -> ContextManager["Verbosity"]: settings.verbosity = self -def _type_check(var: Any, varname: str, types: Union[type, Tuple[type, ...]]): +def _type_check(var: Any, varname: str, types: type | tuple[type, ...]): if isinstance(var, types): return if isinstance(types, type): @@ -62,32 +62,32 @@ def _type_check(var: Any, varname: str, types: Union[type, Tuple[type, ...]]): raise TypeError(f"{varname} must be of type {possible_types_str}") -class stLearnConfig: +class stLearnConfig: # noqa N801 """\ Config manager for scanpy. """ def __init__( - self, - *, - verbosity: str = "warning", - plot_suffix: str = "", - file_format_data: str = "h5ad", - file_format_figs: str = "pdf", - autosave: bool = False, - autoshow: bool = True, - writedir: Union[str, Path] = "./write/", - cachedir: Union[str, Path] = "./cache/", - datasetdir: Union[str, Path] = "./data/", - figdir: Union[str, Path] = "./figures/", - cache_compression: Union[str, None] = "lzf", - max_memory=15, - n_jobs=1, - logfile: Union[str, Path, None] = None, - categories_to_ignore: Iterable[str] = ("N/A", "dontknow", "no_gate", "?"), - _frameon: bool = True, - _vector_friendly: bool = False, - _low_resolution_warning: bool = True, + self, + *, + verbosity: str = "warning", + plot_suffix: str = "", + file_format_data: str = "h5ad", + file_format_figs: str = "pdf", + autosave: bool = False, + autoshow: bool = True, + writedir: str | Path = "./write/", + cachedir: str | Path = "./cache/", + datasetdir: str | Path = "./data/", + figdir: str | Path = "./figures/", + cache_compression: str | None = "lzf", + max_memory=15, + n_jobs=1, + logfile: str | Path | None = None, + categories_to_ignore: Iterable[str] = ("N/A", "dontknow", "no_gate", "?"), + _frameon: bool = True, + _vector_friendly: bool = False, + _low_resolution_warning: bool = True, ): # logging self._root_logger = _RootLogger(logging.INFO) # level will be replaced @@ -139,7 +139,7 @@ def verbosity(self) -> Verbosity: return self._verbosity @verbosity.setter - def verbosity(self, verbosity: Union[Verbosity, int, str]): + def verbosity(self, verbosity: Verbosity | int | str): verbosity_str_options = [ v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str) ] @@ -207,7 +207,8 @@ def file_format_figs(self, figure_format: str): @property def autosave(self) -> bool: """\ - Automatically save figures in :attr:`~stlearn._settings.stLearnConfig.figdir` (default `False`). + Automatically save figures in :attr:`~stlearn._settings.stLearnConfig.figdir` + (default `False`). Do not show plots/figures interactively. """ @@ -240,7 +241,7 @@ def writedir(self) -> Path: return self._writedir @writedir.setter - def writedir(self, writedir: Union[str, Path]): + def writedir(self, writedir: str | Path): _type_check(writedir, "writedir", (str, Path)) self._writedir = Path(writedir) @@ -252,7 +253,7 @@ def cachedir(self) -> Path: return self._cachedir @cachedir.setter - def cachedir(self, cachedir: Union[str, Path]): + def cachedir(self, cachedir: str | Path): _type_check(cachedir, "cachedir", (str, Path)) self._cachedir = Path(cachedir) @@ -264,7 +265,7 @@ def datasetdir(self) -> Path: return self._datasetdir @datasetdir.setter - def datasetdir(self, datasetdir: Union[str, Path]): + def datasetdir(self, datasetdir: str | Path): _type_check(datasetdir, "datasetdir", (str, Path)) self._datasetdir = Path(datasetdir).resolve() @@ -276,12 +277,12 @@ def figdir(self) -> Path: return self._figdir @figdir.setter - def figdir(self, figdir: Union[str, Path]): + def figdir(self, figdir: str | Path): _type_check(figdir, "figdir", (str, Path)) self._figdir = Path(figdir) @property - def cache_compression(self) -> Optional[str]: + def cache_compression(self) -> str | None: """\ Compression for `sc.read(..., cache=True)` (default `'lzf'`). @@ -290,7 +291,7 @@ def cache_compression(self) -> Optional[str]: return self._cache_compression @cache_compression.setter - def cache_compression(self, cache_compression: Optional[str]): + def cache_compression(self, cache_compression: str | None): if cache_compression not in {"lzf", "gzip", None}: raise ValueError( f"`cache_compression` ({cache_compression}) " @@ -299,7 +300,7 @@ def cache_compression(self, cache_compression: Optional[str]): self._cache_compression = cache_compression @property - def max_memory(self) -> Union[int, float]: + def max_memory(self) -> int | float: """\ Maximal memory usage in Gigabyte. @@ -308,7 +309,7 @@ def max_memory(self) -> Union[int, float]: return self._max_memory @max_memory.setter - def max_memory(self, max_memory: Union[int, float]): + def max_memory(self, max_memory: int | float): _type_check(max_memory, "max_memory", (int, float)) self._max_memory = max_memory @@ -325,14 +326,14 @@ def n_jobs(self, n_jobs: int): self._n_jobs = n_jobs @property - def logpath(self) -> Optional[Path]: + def logpath(self) -> Path | None: """\ The file path `logfile` was set to. """ return self._logpath @logpath.setter - def logpath(self, logpath: Union[str, Path, None]): + def logpath(self, logpath: str | Path | None): _type_check(logpath, "logfile", (str, Path)) # set via “file object” branch of logfile.setter self.logfile = Path(logpath).open("a") @@ -347,12 +348,13 @@ def logfile(self) -> TextIO: The default `None` corresponds to :obj:`sys.stdout` in jupyter notebooks and to :obj:`sys.stderr` otherwise. - For backwards compatibility, setting it to `''` behaves like setting it to `None`. + For backwards compatibility, setting it to `''` behaves like setting it + to `None`. """ return self._logfile @logfile.setter - def logfile(self, logfile: Union[str, Path, TextIO, None]): + def logfile(self, logfile: str | Path | TextIO | None): if not hasattr(logfile, "write") and logfile: self.logpath = logfile else: # file object @@ -363,7 +365,7 @@ def logfile(self, logfile: Union[str, Path, TextIO, None]): _set_log_file(self) @property - def categories_to_ignore(self) -> List[str]: + def categories_to_ignore(self) -> list[str]: """\ Categories that are omitted in plotting etc. """ @@ -397,16 +399,16 @@ def categories_to_ignore(self, categories_to_ignore: Iterable[str]): ] def set_figure_params( - self, - dpi: int = 80, - dpi_save: int = 150, - frameon: bool = True, - vector_friendly: bool = True, - fontsize: int = 14, - color_map: Optional[str] = None, - format: _Format = "pdf", - transparent: bool = False, - ipython_format: str = "png2x", + self, + dpi: int = 80, + dpi_save: int = 150, + frameon: bool = True, + vector_friendly: bool = True, + fontsize: int = 14, + color_map: str | None = None, + format: _Format = "pdf", + transparent: bool = False, + ipython_format: str = "png2x", ): """\ Set resolution/size, styling and format of figures. @@ -414,18 +416,21 @@ def set_figure_params( Parameters ---------- dpi - Resolution of rendered figures – this influences the size of figures in notebooks. + Resolution of rendered figures – this influences the size of figures + in notebooks. dpi_save Resolution of saved figures. This should typically be higher to achieve publication quality. frameon Add frames and axes labels to scatter plots. vector_friendly - Plot scatter plots using `png` backend even when exporting as `pdf` or `svg`. + Plot scatter plots using `png` backend even when exporting as + `pdf` or `svg`. fontsize Set the fontsize for several `rcParams` entries. Ignored if `scanpy=False`. color_map - Convenience method for setting the default color map. Ignored if `scanpy=False`. + Convenience method for setting the default color map. Ignored if + `scanpy=False`. format This sets the default format for saving figures: `file_format_figs`. transparent diff --git a/stlearn/add.py b/stlearn/add.py index fde7173d..e69de29b 100644 --- a/stlearn/add.py +++ b/stlearn/add.py @@ -1,10 +0,0 @@ -from .adds.add_image import image -from .adds.add_positions import positions -from .adds.parsing import parsing -from .adds.add_lr import lr -from .adds.annotation import annotation -from .adds.add_labels import labels -from .adds.add_deconvolution import add_deconvolution -from .adds.add_mask import add_mask -from .adds.add_mask import apply_mask -from .adds.add_loupe_clusters import add_loupe_clusters diff --git a/stlearn/adds/add_deconvolution.py b/stlearn/adds/add_deconvolution.py index 6f571395..d169b8ed 100644 --- a/stlearn/adds/add_deconvolution.py +++ b/stlearn/adds/add_deconvolution.py @@ -1,15 +1,14 @@ -from typing import Optional, Union -from anndata import AnnData -import pandas as pd -import numpy as np from pathlib import Path +import pandas as pd +from anndata import AnnData + def add_deconvolution( adata: AnnData, - annotation_path: Union[Path, str], + annotation_path: Path | str, copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Adding label transfered from Seurat diff --git a/stlearn/adds/add_image.py b/stlearn/adds/add_image.py index 39f895fd..15a4953b 100644 --- a/stlearn/adds/add_image.py +++ b/stlearn/adds/add_image.py @@ -1,8 +1,8 @@ -from typing import Optional, Union +import os +from pathlib import Path + from anndata import AnnData from matplotlib import pyplot as plt -from pathlib import Path -import os from PIL import Image Image.MAX_IMAGE_PIXELS = None @@ -10,14 +10,14 @@ def image( adata: AnnData, - imgpath: Union[Path, str], + imgpath: Path | str, library_id: str, quality: str = "hires", scale: float = 1.0, visium: bool = False, spot_diameter_fullres: float = 50, copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Adding image data to the Anndata object @@ -28,11 +28,13 @@ def image( imgpath Image path. library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. + Identifier for the visium library. Can be modified when concatenating + multiple adata objects. scale Set scale factor. quality - Set quality that convert to stlearn to use. Store in anndata.obs['imagecol' & 'imagerow']. + Set quality that convert to stlearn to use. Store in + anndata.obs['imagecol' & 'imagerow']. visium Is this anndata read from Visium platform or not. copy diff --git a/stlearn/adds/add_labels.py b/stlearn/adds/add_labels.py index d4a05451..cb7210bf 100644 --- a/stlearn/adds/add_labels.py +++ b/stlearn/adds/add_labels.py @@ -1,41 +1,45 @@ -from typing import Optional, Union -from anndata import AnnData -from pathlib import Path -import os -import pandas as pd import numpy as np +import pandas as pd +from anndata import AnnData from natsort import natsorted def labels( - adata: AnnData, - label_filepath: str = None, - index_col: int = 0, - use_label: str = None, - sep: str = "\t", - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + label_filepath: str = None, + index_col: int = 0, + use_label: str = None, + sep: str = "\t", + copy: bool = False, +) -> AnnData | None: """\ Add label transfer results into AnnData object Parameters ---------- - adata: AnnData The data object to add L-R info into - label_filepath: str The path to the label transfer results file - use_label: str Where to store the label_transfer results, defaults to 'predictions' in adata.obs & 'label_transfer' in adata.uns. - sep: str Separator of the csv file - copy: bool Copy flag indicating copy or direct edit + adata: AnnData + The data object to add L-R info into + label_filepath: str + The path to the label transfer results file + use_label: str + Where to store the label_transfer results, defaults to 'predictions' + in adata.obs & 'label_transfer' in adata.uns. + sep: str + Separator of the csv file + copy: bool + Copy flag indicating copy or direct edit Returns ------- - adata: AnnData The data object that L-R added into + adata: AnnData + The data object that L-R added into """ labels = pd.read_csv(label_filepath, index_col=index_col, sep=sep) - uns_key = "label_transfer" if type(use_label) == type(None) else use_label + uns_key = "label_transfer" if use_label is None else use_label adata.uns[uns_key] = labels.drop(["predicted.id", "prediction.score.max"], axis=1) - key_add = "predictions" if type(use_label) == type(None) else use_label + key_add = "predictions" if use_label is None else use_label key_source = "predicted.id" adata.obs[key_add] = pd.Categorical( values=np.array(labels[key_source]).astype("U"), diff --git a/stlearn/adds/add_loupe_clusters.py b/stlearn/adds/add_loupe_clusters.py index 116d8eec..4d1baef2 100644 --- a/stlearn/adds/add_loupe_clusters.py +++ b/stlearn/adds/add_loupe_clusters.py @@ -1,18 +1,17 @@ -from typing import Optional, Union -from anndata import AnnData -import pandas as pd -import numpy as np -import stlearn from pathlib import Path + +import numpy as np +import pandas as pd +from anndata import AnnData from natsort import natsorted def add_loupe_clusters( adata: AnnData, - loupe_path: Union[Path, str], + loupe_path: Path | str, key_add: str = "multiplex", copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Adding label transfered from Seurat diff --git a/stlearn/adds/add_lr.py b/stlearn/adds/add_lr.py index 6ed99cde..bafc45ea 100644 --- a/stlearn/adds/add_lr.py +++ b/stlearn/adds/add_lr.py @@ -1,26 +1,28 @@ -from typing import Optional, Union -from anndata import AnnData -from pathlib import Path -import os import pandas as pd +from anndata import AnnData def lr( - adata: AnnData, - db_filepath: str = None, - sep: str = "\t", - source: str = "connectomedb", - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + db_filepath: str = None, + sep: str = "\t", + source: str = "connectomedb", + copy: bool = False, +) -> AnnData | None: """Add significant Ligand-Receptor pairs into AnnData object Parameters ---------- - adata: AnnData The data object to add L-R info into - db_filepath: str The path to the CPDB results file - sep: str Separator of the CPDB results file - source: str Source of LR database (default: connectomedb, can also support 'cellphonedb') - copy: bool Copy flag indicating copy or direct edit + adata: AnnData + The data object to add L-R info into + db_filepath: str + The path to the CPDB results file + sep: str + Separator of the CPDB results file + source: str + Source of LR database (default: connectomedb, can also support 'cellphonedb') + copy: bool + Copy flag indicating copy or direct edit Returns ------- @@ -42,7 +44,7 @@ def lr( elif source == "connectomedb": ctdb = pd.read_csv(db_filepath, sep=sep, quotechar='"', encoding="latin1") adata.uns["lr"] = ( - ctdb["Ligand gene symbol"] + "_" + ctdb["Receptor gene symbol"] + ctdb["Ligand gene symbol"] + "_" + ctdb["Receptor gene symbol"] ).values.tolist() print("connectomedb results added to adata.uns['ctdb']") print("Added ligand receptor pairs to adata.uns['lr'].") diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index 515fa1e9..ffc58f51 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -1,19 +1,18 @@ +import os from pathlib import Path + import matplotlib -from matplotlib import pyplot as plt import numpy as np -from typing import Optional, Union from anndata import AnnData -import os -from stlearn._compat import Literal +from matplotlib import pyplot as plt def add_mask( adata: AnnData, - imgpath: Union[Path, str], + imgpath: Path | str, key: str = "mask", copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Adding binary mask image to the Anndata object @@ -38,7 +37,7 @@ def add_mask( quality = adata.uns["spatial"][library_id]["use_quality"] except: raise KeyError( - f"""\ + """\ Please read ST data first and try again """ ) @@ -78,11 +77,11 @@ def add_mask( def apply_mask( adata: AnnData, - masks: Optional[list] = "all", + masks: list | None = "all", select: str = "black", cmap: str = "default", copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Parsing the old spaital transcriptomics data @@ -106,6 +105,7 @@ def apply_mask( Array format of image, saving by Pillow package. """ from scanpy.plotting import palettes + from stlearn.plotting import palettes_st if cmap == "vega_10_scanpy": @@ -134,7 +134,7 @@ def apply_mask( quality = adata.uns["spatial"][library_id]["use_quality"] except: raise KeyError( - f"""\ + """\ Please read ST data first and try again """ ) @@ -163,16 +163,18 @@ def apply_mask( mask_image = np.where(mask_image > 155, 0, 1) else: raise ValueError( - f"""\ + """\ Only support black and white mask yet. """ ) mask_image_2d = mask_image.mean(axis=2) - apply_spot_mask = lambda x: ( - [i, mask] - if mask_image_2d[int(x["imagerow"]), int(x["imagecol"])] == 1 - else [x[key + "_code"], x[key]] - ) + + def apply_spot_mask(x): + if mask_image_2d[int(x["imagerow"]), int(x["imagecol"])] == 1: + return [i, mask] + else: + return [x[key + "_code"], x[key]] + spot_mask_df = adata.obs.apply(apply_spot_mask, axis=1, result_type="expand") adata.obs[key + "_code"] = spot_mask_df[0] adata.obs[key] = spot_mask_df[1] diff --git a/stlearn/adds/add_positions.py b/stlearn/adds/add_positions.py index 3435c32d..b993a9d3 100644 --- a/stlearn/adds/add_positions.py +++ b/stlearn/adds/add_positions.py @@ -1,17 +1,16 @@ -from typing import Optional, Union -from anndata import AnnData -import pandas as pd from pathlib import Path -import os + +import pandas as pd +from anndata import AnnData def positions( adata: AnnData, - position_filepath: Union[Path, str] = None, - scale_filepath: Union[Path, str] = None, + position_filepath: Path | str = None, + scale_filepath: Path | str = None, quality: str = "low", copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Adding spatial information into the Anndata object diff --git a/stlearn/adds/annotation.py b/stlearn/adds/annotation.py index a8bc1ac9..fcd6fe52 100644 --- a/stlearn/adds/annotation.py +++ b/stlearn/adds/annotation.py @@ -1,16 +1,13 @@ -from typing import Optional, Union, List + from anndata import AnnData -from matplotlib import pyplot as plt -from pathlib import Path -import os def annotation( adata: AnnData, - label_list: List[str], + label_list: list[str], use_label: str = "louvain", copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Adding annotation for cluster diff --git a/stlearn/adds/parsing.py b/stlearn/adds/parsing.py index db241dcd..e0b2daa5 100644 --- a/stlearn/adds/parsing.py +++ b/stlearn/adds/parsing.py @@ -1,17 +1,14 @@ -from typing import Optional, Union -from anndata import AnnData -from matplotlib import pyplot as plt from pathlib import Path -import os -import sys + import numpy as np +from anndata import AnnData def parsing( adata: AnnData, - coordinates_file: Union[Path, str], + coordinates_file: Path | str, copy: bool = True, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Parsing the old spaital transcriptomics data @@ -32,7 +29,7 @@ def parsing( # Get a map of the new coordinates new_coordinates = dict() - with open(coordinates_file, "r") as filehandler: + with open(coordinates_file) as filehandler: for line in filehandler.readlines(): tokens = line.split() assert len(tokens) >= 6 or len(tokens) == 4 @@ -65,7 +62,7 @@ def parsing( imgcol.append(new_x) imgrow.append(new_y) - new_index_values.append("{0}x{1}".format(new_x, new_y)) + new_index_values.append(f"{new_x}x{new_y}") except KeyError: counts_table.drop(index, inplace=True) diff --git a/stlearn/app/app.py b/stlearn/app/app.py index 6eb6a5dc..8f6ccd74 100644 --- a/stlearn/app/app.py +++ b/stlearn/app/app.py @@ -1,53 +1,44 @@ -import os, sys, subprocess +import os +import subprocess +import sys +from threading import Thread sys.path.append(os.path.dirname(__file__)) try: - import flask + import flask # noqa: F401 except ImportError: subprocess.call( "pip install -r " + os.path.dirname(__file__) + "//requirements.txt", shell=True ) -from flask import ( - Flask, - render_template, - request, - flash, - url_for, - redirect, - session, - send_file, -) -from bokeh.embed import components -from bokeh.plotting import figure -from bokeh.resources import INLINE -from werkzeug.utils import secure_filename -import tempfile -import traceback - +import asyncio import tempfile -import shutil -import stlearn -import scanpy import numpy import numpy as np - -import asyncio -from bokeh.server.server import BaseServer -from bokeh.server.tornado import BokehTornado -from tornado.httpserver import HTTPServer -from tornado.ioloop import IOLoop +import scanpy from bokeh.application import Application from bokeh.application.handlers import FunctionHandler -from bokeh.server.server import Server from bokeh.embed import server_document - -from bokeh.layouts import column, row +from bokeh.layouts import row +from bokeh.server.server import Server +from flask import ( + Flask, + flash, + redirect, + render_template, + request, + send_file, + url_for, +) # Functions related to processing the forms. from source.forms import views # for changing data in response to input +from tornado.ioloop import IOLoop +from werkzeug.utils import secure_filename + +import stlearn # Global variables. @@ -497,6 +488,4 @@ def bk_worker(): server.io_loop.start() -from threading import Thread - Thread(target=bk_worker).start() diff --git a/stlearn/app/cli.py b/stlearn/app/cli.py index 20154df4..ff45c24a 100644 --- a/stlearn/app/cli.py +++ b/stlearn/app/cli.py @@ -1,7 +1,9 @@ +import errno +import os + import click -from .. import __version__ -import os +from .. import __version__ @click.group( diff --git a/stlearn/app/source/forms/form_validators.py b/stlearn/app/source/forms/form_validators.py index 5afc6d3c..1d6f2672 100644 --- a/stlearn/app/source/forms/form_validators.py +++ b/stlearn/app/source/forms/form_validators.py @@ -3,7 +3,7 @@ from wtforms.validators import ValidationError -class CheckNumberRange(object): +class CheckNumberRange: def __init__(self, lower, upper, hint=""): self.lower = lower self.upper = upper diff --git a/stlearn/app/source/forms/forms.py b/stlearn/app/source/forms/forms.py index 53ae1908..91aff56e 100644 --- a/stlearn/app/source/forms/forms.py +++ b/stlearn/app/source/forms/forms.py @@ -4,61 +4,60 @@ SingleCellAnalysis dataset. """ -import sys +import wtforms from flask_wtf import FlaskForm # from flask_wtf.file import FileField -from wtforms import SelectMultipleField, SelectField -import wtforms +from wtforms import SelectField, SelectMultipleField def createSuperForm(elements, element_fields, element_values, validators=None): """ Creates a general form; goal is to create a fully programmable form \ - that essentially governs all the options the user will select. + that essentially governs all the options the user will select. - Args: - elements (list): Element names to be rendered on the page, in \ - order of how they will appear on the page. + Args: + elements (list): Element names to be rendered on the page, in \ + order of how they will appear on the page. - element_fields (list): The names of the fields to be rendered. \ - Each field is in same order as 'elements'. \ - Currently supported are: \ - 'Title', 'SelectMultipleField', 'SelectField', \ - 'StringField', 'Text', 'List'. + element_fields (list): The names of the fields to be rendered. \ + Each field is in same order as 'elements'. \ + Currently supported are: \ + 'Title', 'SelectMultipleField', 'SelectField', \ + 'StringField', 'Text', 'List'. - element_values (list): The information which will be put into \ - the field. Changes depending on field: \ + element_values (list): The information which will be put into \ + the field. Changes depending on field: \ - 'Title' and 'Text': 'object' is a string - containing the title which will be added as \ - a heading when rendered on the page. + 'Title' and 'Text': 'object' is a string + containing the title which will be added as \ + a heading when rendered on the page. - 'SelectMultipleField' and 'SelectField': - 'object' is list of options to select from. + 'SelectMultipleField' and 'SelectField': + 'object' is list of options to select from. - 'StringField': - The example values to display within the \ - fields text area. The 'placeholder' option. + 'StringField': + The example values to display within the \ + fields text area. The 'placeholder' option. - 'List': - A list of objects which will be attached \ - to the form. + 'List': + A list of objects which will be attached \ + to the form. - validators (list): A list of functions which take the \ - form as input, used to construct the form validator. \ - Form validator constructed by calling these \ - sequentially with form 'self' as input. + validators (list): A list of functions which take the \ + form as input, used to construct the form validator. \ + Form validator constructed by calling these \ + sequentially with form 'self' as input. - Args: - form (list): A WTForm which has attached as variable all the \ - fields mentioned, so then when rendered as input to - 'SuperDataDisplay.html' shows the form. - """ + Args: + form (list): A WTForm which has attached as variable all the \ + fields mentioned, so then when rendered as input to + 'SuperDataDisplay.html' shows the form. + """ class SuperForm(FlaskForm): """A base form on which all of the fields will be added.""" - if type(validators) == type(None): + if validators is None: validators = [None] * len(elements) # Add the information # @@ -82,7 +81,7 @@ class SuperForm(FlaskForm): # left. setattr(SuperForm, element + "_number", int(multiSelectLeft)) # inverts, so if left, goes right for the next multiSelectField - multiSelectLeft = multiSelectLeft == False + multiSelectLeft = not multiSelectLeft else: multiSelectLeft = True # Reset the MultiSelectField position @@ -100,9 +99,9 @@ class SuperForm(FlaskForm): ) # elif fieldName == 'FileField': - # setattr(SuperForm, element, FileField(validators=validators[i])) - # setattr(SuperForm, element + '_placeholder', # Setting default - # element_values[i]) + # setattr(SuperForm, element, FileField(validators=validators[i])) + # setattr(SuperForm, element + '_placeholder', # Setting default + # element_values[i]) elif fieldName in [ "StringField", @@ -198,10 +197,13 @@ def getCCIForm(adata): related to CCI analysis. """ elements = [ - "Cell information (only discrete labels available, unless mixture already in anndata.uns)", + "Cell information (only discrete labels available, unless mixture already in " + + "anndata.uns)", "Minimum spots for LR to be considered", - "Spot mixture (only if the 'Cell Information' label selected available in anndata.uns)", - "Cell proportion cutoff (value above which cell is considered in spot if 'Spot mixture' selected)", + "Spot mixture (only if the 'Cell Information' label selected available in " + + "anndata.uns)", + "Cell proportion cutoff (value above which cell is considered in spot " + + "if 'Spot mixture' selected)", "Permutations (recommend atleast 1000)", ] element_fields = [ @@ -211,12 +213,12 @@ def getCCIForm(adata): "FloatField", "IntegerField", ] - if type(adata) == type(None): + if adata is None: fields = [] mix = False else: fields = [ - key for key in adata.obs.keys() if type(adata.obs[key].values[0]) == str + key for key in adata.obs.keys() if adata.obs[key].values[0] is str ] mix = fields[0] in adata.uns.keys() element_values = [fields, 20, mix, 0.2, 100] @@ -279,7 +281,7 @@ def getPSTSForm(trajectory, clusts, options): Args: cluster_set (numpy.array): The clusters which can be selected as - the root for psts analysis. + the root for psts analysis. Returns: FlaskForm: With attributes that allow input related to psts. @@ -308,7 +310,7 @@ def getDEAForm(list_labels, methods): Args: cluster_set (numpy.array): The clusters which can be selected as - the root for psts analysis. + the root for psts analysis. Returns: FlaskForm: With attributes that allow input related to psts. @@ -322,43 +324,42 @@ def getDEAForm(list_labels, methods): element_values = [list_labels, methods] return createSuperForm(elements, element_fields, element_values) - ######################## Junk Code ############################################# # def getCCIForm(step_log): -# """ Gets the CCI form generated from the superform above. +# """ Gets the CCI form generated from the superform above. # -# Returns: -# FlaskForm: With attributes that allow for inputs that are related to -# CCI analysis. -# """ -# elements, element_fields, element_values = [], [], [] -# if type(step_log['cci_het']) == type(None): -# # Analysis type form version # -# analysis_elements = ['Cell Heterogeneity Information', # Title -# 'cci_het', -# 'Permutation Testing', # Title -# 'cci_perm'] -# analysis_fields = ['Title', 'SelectField', 'Title', 'SelectField'] -# label_transfer_options = ['Upload Cell Label Transfer', -# 'No Cell Label Transfer'] -# permutation_options = ['With permutation testing', -# 'Without permutation testing'] -# analysis_values = ['', label_transfer_options, '', permutation_options] -# elements += analysis_elements -# element_fields += analysis_fields -# element_values += analysis_values +# Returns: +# FlaskForm: With attributes that allow for inputs that are related to +# CCI analysis. +# """ +# elements, element_fields, element_values = [], [], [] +# if type(step_log['cci_het']) == type(None): +# # Analysis type form version # +# analysis_elements = ['Cell Heterogeneity Information', # Title +# 'cci_het', +# 'Permutation Testing', # Title +# 'cci_perm'] +# analysis_fields = ['Title', 'SelectField', 'Title', 'SelectField'] +# label_transfer_options = ['Upload Cell Label Transfer', +# 'No Cell Label Transfer'] +# permutation_options = ['With permutation testing', +# 'Without permutation testing'] +# analysis_values = ['', label_transfer_options, '', permutation_options] +# elements += analysis_elements +# element_fields += analysis_fields +# element_values += analysis_values # -# else: -# # Core elements regardless of CCI mode # -# elements += ['Neighbourhood distance', -# 'L-R pair input (e.g. L1_R1, L2_R2, ...)'] -# element_fields += ['IntegerField', 'StringField'] -# element_values += [5, ''] +# else: +# # Core elements regardless of CCI mode # +# elements += ['Neighbourhood distance', +# 'L-R pair input (e.g. L1_R1, L2_R2, ...)'] +# element_fields += ['IntegerField', 'StringField'] +# element_values += [5, ''] # -# if step_log['cci_perm']: -# # Including cell heterogeneity information # -# elements += ['Permutations'] -# element_fields += ['IntegerField'] -# element_values += [200] +# if step_log['cci_perm']: +# # Including cell heterogeneity information # +# elements += ['Permutations'] +# element_fields += ['IntegerField'] +# element_values += [200] # -# return createSuperForm(elements, element_fields, element_values, None) +# return createSuperForm(elements, element_fields, element_values, None) diff --git a/stlearn/app/source/forms/helper_functions.py b/stlearn/app/source/forms/helper_functions.py index e9a64e40..692c98a9 100644 --- a/stlearn/app/source/forms/helper_functions.py +++ b/stlearn/app/source/forms/helper_functions.py @@ -1,6 +1,5 @@ # Purpose of this script is to write the functions that help facilitate # subsetting of the data depending on the users input -import numpy def printOut(text, fileName="stdout.txt", close=True, file=None): @@ -8,7 +7,7 @@ def printOut(text, fileName="stdout.txt", close=True, file=None): If close is Fale, returns open file. """ - if type(file) == type(None): + if file is None: file = open(fileName, "w") print(text, file=file) @@ -21,7 +20,7 @@ def printOut(text, fileName="stdout.txt", close=True, file=None): def filterOptions(metaDataSets, options): """Returns options that overlap with keys in metaDataSets dictionary""" - if type(options) == type(None): + if options is None: options = list(metaDataSets.keys()) else: options = [option for option in options if option in metaDataSets.keys()] diff --git a/stlearn/app/source/forms/utils.py b/stlearn/app/source/forms/utils.py index 3782c74f..43284e68 100644 --- a/stlearn/app/source/forms/utils.py +++ b/stlearn/app/source/forms/utils.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """Helper utilities and decorators.""" from flask import flash -import matplotlib.pyplot as plt def flash_errors(form, category="warning"): diff --git a/stlearn/app/source/forms/view_helpers.py b/stlearn/app/source/forms/view_helpers.py index ac9c4ea4..a5613b61 100644 --- a/stlearn/app/source/forms/view_helpers.py +++ b/stlearn/app/source/forms/view_helpers.py @@ -1,6 +1,5 @@ """Helper functions for views.py.""" -import numpy def getVal(form, element): diff --git a/stlearn/app/source/forms/views.py b/stlearn/app/source/forms/views.py index 551c737e..09dc3888 100644 --- a/stlearn/app/source/forms/views.py +++ b/stlearn/app/source/forms/views.py @@ -1,25 +1,21 @@ """ This is more a general views focussed on defining functions which are \ - called by other views for specify pages. This way different pages can be \ - used to display different data, but in a consistent way. + called by other views for specify pages. This way different pages can be \ + used to display different data, but in a consistent way. """ import sys +import traceback + import numpy import numpy as np -from flask import flash +import scanpy as sc +import source.forms.view_helpers as vhs +from flask import flash, render_template from source.forms import forms - from source.forms.utils import flash_errors -import source.forms.view_helpers as vhs -import traceback - -from flask import render_template -import scanpy as sc import stlearn as st -from scipy.spatial.distance import cosine - # Creating the forms using a class generator # PreprocessForm = forms.getPreprocessForm() # CCIForm = forms.getCCIForm() #OLD @@ -35,7 +31,7 @@ def run_preprocessing(request, adata, step_log): if not form.validate_on_submit(): flash_errors(form) - elif type(adata) == type(None): + elif adata is None: flash("Need to load data first!") else: @@ -87,13 +83,12 @@ def run_lr(request, adata, step_log): if not form.validate_on_submit(): flash_errors(form) - elif type(adata) == type(None): + elif adata is None: flash("Need to load data first!") else: step_log["lr_params"] = vhs.getData(form) print(step_log["lr_params"], file=sys.stdout) - elements = numpy.array(list(step_log["lr_params"].keys())) # order: Species, Spot neighbourhood, min_spots, n_pairs, CPUs element_values = list(step_log["lr_params"].values()) dist = element_values[1] @@ -134,13 +129,12 @@ def run_cci(request, adata, step_log): if not form.validate_on_submit(): flash_errors(form) - elif type(adata) == type(None): + elif adata is None: flash("Need to load data first!") else: step_log["cci_params"] = vhs.getData(form) print(step_log["cci_params"], file=sys.stdout) - elements = numpy.array(list(step_log["cci_params"].keys())) # order: cell_type, min_spots, spot_mixtures, cell_prop_cutoff, sig_spots # n_perms element_values = list(step_log["cci_params"].values()) @@ -188,14 +182,13 @@ def run_clustering(request, adata, step_log): step_log["cluster_params"] = vhs.getData(form) print(step_log["cluster_params"], file=sys.stdout) - elements = list(step_log["cluster_params"].keys()) # order: pca_comps, SME bool, method, method_param element_values = list(step_log["cluster_params"].values()) if not form.validate_on_submit(): flash_errors(form) - elif type(adata) == type(None): + elif adata is None: flash("Need to load data first!") else: @@ -275,7 +268,6 @@ def run_psts(request, adata, step_log): step_log["psts_params"] = vhs.getData(form) print(step_log["psts_params"], file=sys.stdout) - elements = list(step_log["psts_params"].keys()) # order: pca_comps, SME bool, method, method_param element_values = list(step_log["psts_params"].values()) @@ -289,7 +281,7 @@ def run_psts(request, adata, step_log): if not form.validate_on_submit(): flash_errors(form) - elif type(adata) == type(None): + elif adata is None: flash("Need to load data first!") else: @@ -349,7 +341,6 @@ def run_psts(request, adata, step_log): def run_dea(request, adata, step_log): - list_labels = [] for col in adata.obs.columns: @@ -366,13 +357,12 @@ def run_dea(request, adata, step_log): step_log["dea_params"] = vhs.getData(form) print(step_log["dea_params"], file=sys.stdout) - elements = list(step_log["dea_params"].keys()) element_values = list(step_log["dea_params"].values()) if not form.validate_on_submit(): flash_errors(form) - elif type(adata) == type(None): + elif adata is None: flash("Need to load data first!") else: diff --git a/stlearn/classes.py b/stlearn/classes.py index afa4b997..f9ef77c2 100644 --- a/stlearn/classes.py +++ b/stlearn/classes.py @@ -4,37 +4,34 @@ Date: 20 Feb 2021 """ -from typing import Optional, Union, Mapping # Special -from typing import Sequence, Iterable # ABCs -from typing import Tuple # Classes import numpy as np from anndata import AnnData from .utils import ( Empty, - _empty, - _check_spatial_data, + _check_coords, _check_img, - _check_spot_size, _check_scale_factor, - _check_coords, + _check_spatial_data, + _check_spot_size, + _empty, ) -class Spatial(object): +class Spatial: def __init__( self, adata: AnnData, basis: str = "spatial", - img: Union[np.ndarray, None] = None, - img_key: Union[str, None, Empty] = _empty, - library_id: Union[str, None] = _empty, - crop_coord: Optional[bool] = True, - bw: Optional[bool] = False, - scale_factor: Optional[float] = None, - spot_size: Optional[float] = None, - use_raw: Optional[bool] = False, + img: np.ndarray | None = None, + img_key: str | None | Empty = _empty, + library_id: str | None = _empty, + crop_coord: bool | None = True, + bw: bool | None = False, + scale_factor: float | None = None, + spot_size: float | None = None, + use_raw: bool | None = False, **kwargs, ): diff --git a/stlearn/datasets.py b/stlearn/datasets.py index 068c89d0..e69de29b 100644 --- a/stlearn/datasets.py +++ b/stlearn/datasets.py @@ -1 +0,0 @@ -from ._datasets._datasets import example_bcba diff --git a/stlearn/em.py b/stlearn/em.py index 193ade80..6768d1c6 100644 --- a/stlearn/em.py +++ b/stlearn/em.py @@ -1,7 +1,2 @@ -from .embedding.pca import run_pca -from .embedding.umap import run_umap -from .embedding.ica import run_ica # from .embedding.scvi import run_ldvae -from .embedding.fa import run_fa -from .embedding.diffmap import run_diffmap diff --git a/stlearn/embedding/diffmap.py b/stlearn/embedding/diffmap.py index 97f916e8..93a5c480 100644 --- a/stlearn/embedding/diffmap.py +++ b/stlearn/embedding/diffmap.py @@ -1,9 +1,5 @@ -from typing import Optional, Union -import numpy as np -from anndata import AnnData -from numpy.random.mtrand import RandomState -from scipy.sparse import issparse import scanpy +from anndata import AnnData def run_diffmap(adata: AnnData, n_comps: int = 15, copy: bool = False): @@ -41,7 +37,8 @@ def run_diffmap(adata: AnnData, n_comps: int = 15, copy: bool = False): scanpy.tl.diffmap(adata, n_comps=n_comps, copy=copy) print( - "Diffusion Map is done! Generated in adata.obsm['X_diffmap'] nad adata.uns['diffmap_evals']" + "Diffusion Map is done! Generated in adata.obsm['X_diffmap'] and " + + "adata.uns['diffmap_evals']" ) return adata if copy else None diff --git a/stlearn/embedding/fa.py b/stlearn/embedding/fa.py index 715de4d5..a707efb3 100644 --- a/stlearn/embedding/fa.py +++ b/stlearn/embedding/fa.py @@ -1,10 +1,7 @@ -import numpy as np -import pandas as pd -from typing import Optional from anndata import AnnData -from sklearn.decomposition import FactorAnalysis from scipy.sparse import issparse +from sklearn.decomposition import FactorAnalysis def run_fa( @@ -17,7 +14,7 @@ def run_fa( random_state: int = 2108, use_data: str = None, copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Factor Analysis (FA) A simple linear generative model with Gaussian latent variables. diff --git a/stlearn/embedding/ica.py b/stlearn/embedding/ica.py index a42cda5a..e99e77ca 100644 --- a/stlearn/embedding/ica.py +++ b/stlearn/embedding/ica.py @@ -1,9 +1,7 @@ -import numpy as np -import pandas as pd -from typing import Optional + from anndata import AnnData -from sklearn.decomposition import FastICA from scipy.sparse import issparse +from sklearn.decomposition import FastICA def run_ica( @@ -13,7 +11,7 @@ def run_ica( tol: float = 0.0001, use_data: str = None, copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ FastICA: a fast algorithm for Independent Component Analysis. @@ -64,7 +62,8 @@ def my_g(x): adata.uns["ica"] = {"params": {"n_factors": n_factors, "fun": fun, "tol": tol}} print( - "ICA is done! Generated in adata.obsm['X_ica'] and parameters in adata.uns['ica']" + "ICA is done! Generated in adata.obsm['X_ica'] and parameters in " + + "adata.uns['ica']" ) return adata if copy else None diff --git a/stlearn/embedding/pca.py b/stlearn/embedding/pca.py index 040a3b6f..d4b66f15 100644 --- a/stlearn/embedding/pca.py +++ b/stlearn/embedding/pca.py @@ -1,25 +1,24 @@ -import logging as logg -from typing import Union, Optional, Tuple, Collection, Sequence, Iterable -from anndata import AnnData + import numpy as np -from scipy.sparse import issparse, isspmatrix_csr, csr_matrix, spmatrix -from numpy.random.mtrand import RandomState import scanpy +from anndata import AnnData +from numpy.random.mtrand import RandomState +from scipy.sparse import spmatrix def run_pca( - data: Union[AnnData, np.ndarray, spmatrix], + data: AnnData | np.ndarray | spmatrix, n_comps: int = 50, - zero_center: Optional[bool] = True, + zero_center: bool | None = True, svd_solver: str = "auto", - random_state: Optional[Union[int, RandomState]] = 0, + random_state: int | RandomState | None = 0, return_info: bool = False, - use_highly_variable: Optional[bool] = None, + use_highly_variable: bool | None = None, dtype: str = "float32", copy: bool = False, chunked: bool = False, - chunk_size: Optional[int] = None, -) -> Union[AnnData, np.ndarray, spmatrix]: + chunk_size: int | None = None, +) -> AnnData | np.ndarray | spmatrix: """\ Wrap function scanpy.pp.pca Principal component analysis [Pedregosa11]_. @@ -100,5 +99,6 @@ def run_pca( ) print( - "PCA is done! Generated in adata.obsm['X_pca'], adata.uns['pca'] and adata.varm['PCs']" + "PCA is done! Generated in adata.obsm['X_pca'], adata.uns['pca'] and " + + "adata.varm['PCs']" ) diff --git a/stlearn/embedding/umap.py b/stlearn/embedding/umap.py index 912aaa00..85f6e8b1 100644 --- a/stlearn/embedding/umap.py +++ b/stlearn/embedding/umap.py @@ -1,31 +1,30 @@ -from typing import Optional, Union import numpy as np +import scanpy from anndata import AnnData from numpy.random.mtrand import RandomState from .._compat import Literal -import scanpy _InitPos = Literal["paga", "spectral", "random"] def run_umap( - adata: AnnData, - min_dist: float = 0.5, - spread: float = 1.0, - n_components: int = 2, - maxiter: Optional[int] = None, - alpha: float = 1.0, - gamma: float = 1.0, - negative_sample_rate: int = 5, - init_pos: Union[_InitPos, np.ndarray, None] = "spectral", - random_state: Optional[Union[int, RandomState]] = 0, - a: Optional[float] = None, - b: Optional[float] = None, - copy: bool = False, - method: Literal["umap", "rapids"] = "umap", -) -> Optional[AnnData]: + adata: AnnData, + min_dist: float = 0.5, + spread: float = 1.0, + n_components: int = 2, + maxiter: int | None = None, + alpha: float = 1.0, + gamma: float = 1.0, + negative_sample_rate: int = 5, + init_pos: _InitPos | np.ndarray | None = "spectral", + random_state: int | RandomState | None = 0, + a: float | None = None, + b: float | None = None, + copy: bool = False, + method: Literal["umap", "rapids"] = "umap", # noqa: F821 +) -> AnnData | None: """\ Wrap function scanpy.pp.umap Embed the neighborhood graph using UMAP [McInnes18]_. diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index b2946ee8..0d451041 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -1,15 +1,15 @@ -from .model_zoo import encode, Model -from typing import Optional, Union -from anndata import AnnData + import numpy as np -from .._compat import Literal -from PIL import Image import pandas as pd -from pathlib import Path +from anndata import AnnData +from PIL import Image # Test progress bar from tqdm import tqdm +from .._compat import Literal +from .model_zoo import Model, encode + _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] @@ -20,7 +20,7 @@ def extract_feature( verbose: bool = False, copy: bool = False, seeds: int = 1, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Extract latent morphological features from H&E images using pre-trained convolutional neural network base @@ -63,7 +63,7 @@ def extract_feature( tile = tile.astype(np.float32) tile = np.stack([tile]) if verbose: - print("extract feature for spot: {}".format(str(spot))) + print(f"extract feature for spot: {str(spot)}") features = encode(tile, model) feature_dfs.append(pd.DataFrame(features, columns=[spot])) pbar.update(1) diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index bdb88a60..4daee2a8 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -1,25 +1,24 @@ -from typing import Optional, Union +import os +from pathlib import Path + +import numpy as np from anndata import AnnData -from .._compat import Literal from PIL import Image -from pathlib import Path # Test progress bar from tqdm import tqdm -import numpy as np -import os def tiling( adata: AnnData, - out_path: Union[Path, str] = "./tiling", - library_id: Union[str, None] = None, + out_path: Path | str = "./tiling", + library_id: str | None = None, crop_size: int = 40, target_size: int = 299, img_fmt: str = "JPEG", verbose: bool = False, copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Tiling H&E images to small tiles based on spot spatial location @@ -93,9 +92,7 @@ def tiling( if verbose: print( - "generate tile at location ({}, {})".format( - str(imagecol), str(imagerow) - ) + f"generate tile at location ({str(imagecol)}, {str(imagerow)})" ) pbar.update(1) diff --git a/stlearn/image_preprocessing/model_zoo.py b/stlearn/image_preprocessing/model_zoo.py index a028f75f..7faf2673 100644 --- a/stlearn/image_preprocessing/model_zoo.py +++ b/stlearn/image_preprocessing/model_zoo.py @@ -8,12 +8,12 @@ class Model: __name__ = "CNN base model" def __init__(self, base, batch_size=1): - from tensorflow.keras import backend as K + from tensorflow.keras import backend as keras self.base = base self.model, self.preprocess = self.load_model() self.batch_size = batch_size - self.data_format = K.image_data_format() + self.data_format = keras.image_data_format() def load_model(self): if self.base == "resnet50": @@ -48,13 +48,13 @@ def load_model(self): include_top=False, weights="imagenet", pooling="avg" ) else: - raise ValueError("{} is not a valid model".format(self.base)) + raise ValueError(f"{self.base} is not a valid model") return cnn_base_model, preprocess_input def predict(self, x): - from tensorflow.keras import backend as K + from tensorflow.keras import backend as keras if self.data_format == "channels_first": x = x.transpose(0, 3, 1, 2) - x = self.preprocess(x.astype(K.floatx())) + x = self.preprocess(x.astype(keras.floatx())) return self.model.predict(x, batch_size=self.batch_size, verbose=False) diff --git a/stlearn/image_preprocessing/segmentation.py b/stlearn/image_preprocessing/segmentation.py index 76023058..8c7f4dfe 100644 --- a/stlearn/image_preprocessing/segmentation.py +++ b/stlearn/image_preprocessing/segmentation.py @@ -1,4 +1,4 @@ -from typing import Optional + import histomicstk as htk import numpy as np import scipy as sp @@ -17,7 +17,7 @@ def morph_watershed( library_id: str = None, verbose: bool = False, copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: """\ Watershed method to segment nuclei and calculate morphological statistics @@ -163,13 +163,6 @@ def _calculate_morph_stats(tile_path): # compute nuclei properties objProps = skimage.measure.regionprops(im_nuclei_seg_mask) - # # Display results - # plt.figure(figsize=(20, 10)) - # plt.imshow(skimage.color.label2rgb(im_nuclei_seg_mask, im_nuclei_stain, bg_label=0), - # origin='upper') - # plt.title('Nuclei segmentation mask overlay') - # plt.savefig("./Nuclei_segmentation_tiles_bc_wh/{}.png".format(tile_path.split("/")[-1].split(".")[0]), dpi=300) - n_nuclei = len(objProps) nuclei_total_area = sum(map(lambda x: x.area, objProps)) diff --git a/stlearn/logging.py b/stlearn/logging.py index 63c0f6eb..e23e2786 100644 --- a/stlearn/logging.py +++ b/stlearn/logging.py @@ -1,14 +1,12 @@ """Logging and Profiling""" import logging -from functools import update_wrapper, partial -from logging import CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET from datetime import datetime, timedelta, timezone -from typing import Optional +from functools import partial, update_wrapper +from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING import anndata.logging - HINT = (INFO + DEBUG) // 2 logging.addLevelName(HINT, "HINT") @@ -24,9 +22,9 @@ def log( level: int, msg: str, *, - extra: Optional[dict] = None, + extra: dict | None = None, time: datetime = None, - deep: Optional[str] = None, + deep: str | None = None, ) -> datetime: from . import settings @@ -180,8 +178,8 @@ def error( msg: str, *, time: datetime = None, - deep: Optional[str] = None, - extra: Optional[dict] = None, + deep: str | None = None, + extra: dict | None = None, ) -> datetime: """\ Log message with specific level and return current time. diff --git a/stlearn/pl.py b/stlearn/pl.py index 7f7577d4..54be1ef2 100644 --- a/stlearn/pl.py +++ b/stlearn/pl.py @@ -1,26 +1,2 @@ -from .plotting.gene_plot import gene_plot -from .plotting.gene_plot import gene_plot_interactive -from .plotting.feat_plot import feat_plot -from .plotting.cluster_plot import cluster_plot -from .plotting.cluster_plot import cluster_plot_interactive -from .plotting.subcluster_plot import subcluster_plot -from .plotting.non_spatial_plot import non_spatial_plot -from .plotting.deconvolution_plot import deconvolution_plot -from .plotting.stack_3d_plot import stack_3d_plot -from .plotting import trajectory -from .plotting.QC_plot import QC_plot -from .plotting.cci_plot import het_plot # from .plotting.cci_plot import het_plot_interactive -from .plotting.cci_plot import lr_plot_interactive, spatialcci_plot_interactive -from .plotting.cci_plot import grid_plot -from .plotting.cci_plot import lr_diagnostics, lr_n_spots, lr_summary, lr_go -from .plotting.cci_plot import lr_plot, lr_result_plot -from .plotting.cci_plot import ( - ccinet_plot, - cci_map, - lr_cci_map, - lr_chord_plot, - cci_check, -) -from .plotting.mask_plot import plot_mask diff --git a/stlearn/plotting/QC_plot.py b/stlearn/plotting/QC_plot.py index 186d542b..9b4af383 100644 --- a/stlearn/plotting/QC_plot.py +++ b/stlearn/plotting/QC_plot.py @@ -1,7 +1,7 @@ -from matplotlib import pyplot as plt + import numpy as np -from typing import Optional, Union from anndata import AnnData +from matplotlib import pyplot as plt def QC_plot( @@ -19,7 +19,7 @@ def QC_plot( margin: int = 100, dpi: int = 150, output: str = None, -) -> Optional[AnnData]: +) -> AnnData | None: """\ QC plot for sptial transcriptomics data. diff --git a/stlearn/plotting/_docs.py b/stlearn/plotting/_docs.py index f9a66165..dbf36984 100644 --- a/stlearn/plotting/_docs.py +++ b/stlearn/plotting/_docs.py @@ -6,7 +6,8 @@ figsize Figure size with the format (width,height). cmap - Color map to use for continous variables or discretes variables (e.g. viridis, Set1,...). + Color map to use for continous variables or discretes variables (e.g. viridis, + Set1,...). use_label Key for the label use in `adata.obs` (e.g. `leiden`, `louvain`,...). list_clusters @@ -39,7 +40,8 @@ doc_gene_plot = """\ gene_symbols - Single gene (str) or multiple genes (list) that user wants to display. It should be available in `adata.var_names`. + Single gene (str) or multiple genes (list) that user wants to display. It should + be available in `adata.var_names`. threshold Threshold to display genes in the figure. method @@ -83,23 +85,28 @@ sig_spots Whether to filter to significant spots or not. use_label - Label to use for the inner points, can be in adata.obs or in the lr stats of adata.uns['per_lr_results'][lr].columns + Label to use for the inner points, can be in adata.obs or in the lr stats of + adata.uns['per_lr_results'][lr].columns use_mix - The deconvolution/label_transfer results to use for visualising pie charts in the inner point, not currently implimented. + The deconvolution/label_transfer results to use for visualising pie charts in + the inner point, not currently implimented. outer_mode - Either 'binary', 'continuous', or None; controls how ligand-receptor expression shown (or not shown). + Either 'binary', 'continuous', or None; controls how ligand-receptor expression + shown (or not shown). l_cmap matplotlib cmap controlling ligand continous expression. r_cmap matplotlib cmap controlling receptor continuous expression. lr_cmap - matplotlib cmap controlling the ligand receptor binary expression, but have atleast 4 colours. + matplotlib cmap controlling the ligand receptor binary expression, but have + at least 4 colours. inner_cmap matplotlib cmap controlling the inner point colours. inner_size_prop multiplier which controls size of inner points. middle_size_prop - Multiplier which controls size of middle point (only relevant when outer_mode='continuous') + Multiplier which controls size of middle point (only relevant when + outer_mode='continuous') outer_size_prop Multiplier which controls size of the outter point. pt_scale @@ -109,12 +116,14 @@ show_image Whether to show the background H&E or not. kwargs - Extra arguments parsed to the other plotting functions such as gene_plot, cluster_plot, &/or het_plot. + Extra arguments parsed to the other plotting functions such as gene_plot, + cluster_plot, &/or het_plot. """ doc_het_plot = """\ use_het - Single gene (str) or multiple genes (list) that user wants to display. It should be available in `adata.var_names`. + Single gene (str) or multiple genes (list) that user wants to display. It should + be available in `adata.var_names`. contour Option to show the contour plot. step_size diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 06d997f8..a2816a37 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -1,75 +1,69 @@ -from matplotlib import pyplot as plt -from matplotlib.axes import Axes -from matplotlib.figure import Figure -import matplotlib -import pandas as pd -import numpy as np -import networkx as nx +import importlib import math -import matplotlib.patches as patches -from numba.typed import List -import seaborn as sns import sys -from anndata import AnnData -from typing import Optional, Union - -from typing import Optional, Union, Mapping # Special -from typing import Sequence, Iterable # ABCs -from typing import Tuple # Classes +from typing import ( + Optional, # Special + ) -import warnings +import matplotlib +import matplotlib.patches as patches +import networkx as nx +import numpy as np +import pandas as pd +from anndata import AnnData +from bokeh.io import output_notebook +from bokeh.plotting import show +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from matplotlib.figure import Figure +from scipy.stats import gaussian_kde -from .classes import CciPlot, LrResultPlot -from .classes_bokeh import BokehSpatialCciPlot, BokehLRPlot -from ._docs import doc_spatial_base_plot, doc_het_plot, doc_lr_plot -from ..utils import Empty, _empty, _AxesSubplot, _docs_params -from .utils import get_cmap, check_cmap, get_colors -from .cluster_plot import cluster_plot -from .deconvolution_plot import deconvolution_plot -from .gene_plot import gene_plot -from stlearn.plotting.utils import get_colors import stlearn.plotting.cci_plot_helpers as cci_hs +from stlearn.plotting.utils import get_colors + +from ..utils import _docs_params +from ._docs import doc_het_plot, doc_spatial_base_plot from .cci_plot_helpers import ( - get_int_df, - add_arrows, - create_flat_df, _box_map, chordDiagram, + create_flat_df, + get_int_df, ) -from scipy.stats import gaussian_kde - -import importlib +from .classes import CciPlot, LrResultPlot +from .classes_bokeh import BokehLRPlot, BokehSpatialCciPlot +from .cluster_plot import cluster_plot +from .gene_plot import gene_plot +from .utils import check_cmap, get_cmap importlib.reload(cci_hs) -from bokeh.io import push_notebook, output_notebook -from bokeh.plotting import show #### Functions for visualising the overall LR results and diagnostics. def lr_diagnostics( - adata, - highlight_lrs: list = None, - n_top: int = None, - color0: str = "turquoise", - color1: str = "plum", - figsize: tuple = (10, 4), - lr_text_fp: dict = None, - show: bool = True, + adata, + highlight_lrs: list = None, + n_top: int = None, + color0: str = "turquoise", + color1: str = "plum", + figsize: tuple = (10, 4), + lr_text_fp: dict = None, + show: bool = True, ): - """Diagnostic plot looking at relationship between technical features of lrs and lr rank. - Two plots generated: left is the average of the median for nonzero - expressing spots for both the ligand and the receptor on the y-axis, & - LR-rank by no. of significant spots on the x-axis. Right is the average - of the proportion of zeros for the ligand and receptor gene on teh y-axis. + """Diagnostic plot looking at relationship between technical features of lrs and + lr rank. Two plots generated: left is the average of the median for nonzero + expressing spots for both the ligand and the receptor on the y-axis, & + LR-rank by no. of significant spots on the x-axis. Right is the average + of the proportion of zeros for the ligand and receptor gene on teh y-axis. Parameters ---------- adata: AnnData The data object on which st.tl.cci.run has been applied. highlight_lrs: list - List of LRs to highlight, will add text and change point color for these LR pairs. + List of LRs to highlight, will add text and change point color for these + LR pairs. n_top: int The number of LRs to display. If None shows all. color0: str @@ -83,7 +77,7 @@ def lr_diagnostics( Figure, Axes Figure and axes of the plot, if show=False. """ - if type(n_top) == type(None): + if n_top is None: n_top = adata.uns["lr_summary"].shape[0] fig, axes = plt.subplots(ncols=2, figsize=figsize) cci_hs.lr_scatter( @@ -113,17 +107,17 @@ def lr_diagnostics( def lr_summary( - adata, - n_top: int = 50, - highlight_lrs: list = None, - y: str = "n_spots_sig", - color: str = "gold", - figsize: tuple = None, - highlight_color: str = "red", - max_text: int = 50, - lr_text_fp: dict = None, - ax: Axes = None, - show: bool = True, + adata, + n_top: int = 50, + highlight_lrs: list = None, + y: str = "n_spots_sig", + color: str = "gold", + figsize: tuple = None, + highlight_color: str = "red", + max_text: int = 50, + lr_text_fp: dict = None, + ax: Axes = None, + show: bool = True, ): """Plotting the top LRs ranked by number of significant spots. @@ -181,17 +175,17 @@ def lr_summary( def lr_n_spots( - adata, - n_top: int = 100, - font_dict: dict = None, - xtick_dict: dict = None, - bar_width: float = 1, - max_text: int = 50, - non_sig_color: str = "dodgerblue", - sig_color: str = "springgreen", - figsize: tuple = (6, 4), - show_title: bool = True, - show: bool = True, + adata, + n_top: int = 100, + font_dict: dict = None, + xtick_dict: dict = None, + bar_width: float = 1, + max_text: int = 50, + non_sig_color: str = "dodgerblue", + sig_color: str = "springgreen", + figsize: tuple = (6, 4), + show_title: bool = True, + show: bool = True, ): """Bar plot showing for each LR no. of sig versus non-sig spots. @@ -227,9 +221,9 @@ def lr_n_spots( Fig, Axes Figure & axes with the plot draw on; only if show=False. Else None. """ - if type(font_dict) == type(None): + if font_dict is None: font_dict = {"weight": "bold", "size": 12} - if type(xtick_dict) == type(None): + if xtick_dict is None: xtick_dict = {"fontweight": "bold", "rotation": 90, "size": 6} lrs = adata.uns["lr_summary"].index.values[0:n_top] @@ -263,15 +257,15 @@ def lr_n_spots( def lr_go( - adata, - n_top: int = 20, - highlight_go: list = None, - figsize=(6, 4), - rot: float = 50, - lr_text_fp: dict = None, - highlight_color: str = "yellow", - max_text: int = 50, - show: bool = True, + adata, + n_top: int = 20, + highlight_go: list = None, + figsize=(6, 4), + rot: float = 50, + lr_text_fp: dict = None, + highlight_color: str = "yellow", + max_text: int = 50, + show: bool = True, ): """Plots the results from the LR GO analysis. @@ -327,15 +321,16 @@ def lr_go( def cci_check( - adata: AnnData, - use_label: str, - figsize=(16, 10), - cell_label_size=20, - axis_text_size=18, - tick_size=14, - show=True, + adata: AnnData, + use_label: str, + figsize=(16, 10), + cell_label_size=20, + axis_text_size=18, + tick_size=14, + show=True, ): - """Checks relationship between no. of significant CCI-LR interactions and cell type frequency. + """Checks relationship between no. of significant CCI-LR interactions and cell + type frequency. Parameters ---------- @@ -427,32 +422,32 @@ def cci_check( # Functions for visualisation the LR results per spot. def lr_result_plot( - adata: AnnData, - use_lr: Optional["str"] = None, - use_result: Optional["str"] = "lr_sig_scores", - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - zoom_coord: Optional[float] = None, - crop: Optional[bool] = True, - margin: Optional[float] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - contour: bool = False, - step_size: Optional[int] = None, - vmin: float = None, - vmax: float = None, + adata: AnnData, + use_lr: Optional["str"] = None, + use_result: Optional["str"] = "lr_sig_scores", + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + zoom_coord: float | None = None, + crop: bool | None = True, + margin: float | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, ): """Plots the per spot statistics for given LR. @@ -543,36 +538,36 @@ def lr_result_plot( # @_docs_params(het_plot=doc_lr_plot) def lr_plot( - adata: AnnData, - lr: str, - min_expr: float = 0, - sig_spots=True, - use_label: str = None, - outer_mode: str = "continuous", - l_cmap=None, - r_cmap=None, - lr_cmap=None, - inner_cmap=None, - inner_size_prop: float = 0.25, - middle_size_prop: float = 0.5, - outer_size_prop: float = 1, - pt_scale: int = 100, - title="", - show_image: bool = True, - show_arrows: bool = False, - fig: Figure = None, - ax: Axes = None, - arrow_head_width: float = 4, - arrow_width: float = 0.001, - arrow_cmap: str = None, - arrow_vmax: float = None, - sig_cci: bool = False, - lr_colors: dict = None, - figsize: tuple = (6.4, 4.8), - use_mix: bool = None, - # plotting params - **kwargs, -) -> Optional[AnnData]: + adata: AnnData, + lr: str, + min_expr: float = 0, + sig_spots=True, + use_label: str = None, + outer_mode: str = "continuous", + l_cmap=None, + r_cmap=None, + lr_cmap=None, + inner_cmap=None, + inner_size_prop: float = 0.25, + middle_size_prop: float = 0.5, + outer_size_prop: float = 1, + pt_scale: int = 100, + title="", + show_image: bool = True, + show_arrows: bool = False, + fig: Figure = None, + ax: Axes = None, + arrow_head_width: float = 4, + arrow_width: float = 0.001, + arrow_cmap: str = None, + arrow_vmax: float = None, + sig_cci: bool = False, + lr_colors: dict = None, + figsize: tuple = (6.4, 4.8), + use_mix: bool = None, + # plotting params + **kwargs, +) -> AnnData | None: """Creates different kinds of spatial visualisations for the LR analysis results. To see combinations of parameters refer to stLearn CCI tutorial. @@ -643,7 +638,7 @@ def lr_plot( interactions; particularly relevant when plotting the arrows. lr_colors: dict Specifies the colors of the LRs when plotting with outer_mode='binary'; - structures is {'l': color, 'r': color, 'lr': color, '': color}; + structures is {'ligand': color, 'receptor': color, 'lr': color, '': color}; the last key-value indicates colour for spots not expressing the ligand or receptor. figsize: tuple @@ -653,7 +648,7 @@ def lr_plot( """ # Input checking # - l, r = lr.split("_") + ligand, receptor = lr.split("_") ran_lr = "lr_summary" in adata.uns ran_sig = False if not ran_lr else "n_spots_sig" in adata.uns["lr_summary"].columns if ran_lr and lr in adata.uns["lr_summary"].index: @@ -677,10 +672,10 @@ def lr_plot( # Making sure have run_cci first with respective labelling # if ( - show_arrows - and sig_cci - and use_label - and f"per_lr_cci_{use_label}" not in adata.uns + show_arrows + and sig_cci + and use_label + and f"per_lr_cci_{use_label}" not in adata.uns ): raise Exception( "Cannot subset arrow interactions to significant ccis " @@ -700,25 +695,25 @@ def lr_plot( "lr_sig_scores", ] - if type(use_mix) != type(None) and use_mix not in adata.uns: + if use_mix is not None and use_mix not in adata.uns: raise Exception( - f"Specified use_mix, but no deconvolution results added " + "Specified use_mix, but no deconvolution results added " "to adata.uns matching the use_mix ({use_mix}) key." ) elif ( - type(use_label) != type(None) - and use_label in lr_use_labels - and ran_sig - and not lr_sig + use_label is not None + and use_label in lr_use_labels + and ran_sig + and not lr_sig ): raise Exception( - f"Since use_label refers to lr stats & ran permutation testing, " - f"LR needs to be significant to view stats." + "Since use_label refers to lr stats & ran permutation testing, " + "LR needs to be significant to view stats." ) elif ( - type(use_label) != type(None) - and use_label not in adata.obs.keys() - and use_label not in lr_use_labels + use_label is not None + and use_label not in adata.obs.keys() + and use_label not in lr_use_labels ): raise Exception( f"use_label must be in adata.obs or " f"one of lr stats: {lr_use_labels}." @@ -728,7 +723,7 @@ def lr_plot( if outer_mode not in out_options: raise Exception(f"{outer_mode} should be one of {out_options}") - if l not in adata.var_names or r not in adata.var_names: + if ligand not in adata.var_names or receptor not in adata.var_names: raise Exception("L or R not found in adata.var_names.") # Whether to show just the significant spots or all spots @@ -741,21 +736,21 @@ def lr_plot( adata_full = adata # Dealing with the axis # - if type(fig) == type(None) or type(ax) == type(None): + if fig is None or ax is None: fig, ax = plt.subplots(figsize=figsize) expr = adata.to_df() - l_expr = expr.loc[:, l].values - r_expr = expr.loc[:, r].values + l_expr = expr.loc[:, ligand].values + r_expr = expr.loc[:, receptor].values # Adding binary points of the ligand/receptor pair # if outer_mode == "binary": l_bool, r_bool = l_expr > min_expr, r_expr > min_expr lr_binary_labels = [] for i in range(len(l_bool)): if l_bool[i] and not r_bool[i]: - lr_binary_labels.append(l) + lr_binary_labels.append(ligand) elif not l_bool[i] and r_bool[i]: - lr_binary_labels.append(r) + lr_binary_labels.append(receptor) elif l_bool[i] and r_bool[i]: lr_binary_labels.append(lr) elif not l_bool[i] and not r_bool[i]: @@ -765,12 +760,12 @@ def lr_plot( ).astype("category") adata.obs[f"{lr}_binary_labels"] = lr_binary_labels - if type(lr_cmap) == type(None): + if lr_cmap is None: lr_cmap = "default" # This gets ignored due to setting colours below - if type(lr_colors) == type(None): + if lr_colors is None: lr_colors = { - l: matplotlib.colors.to_hex("r"), - r: matplotlib.colors.to_hex("limegreen"), + ligand: matplotlib.colors.to_hex("receptor"), + receptor: matplotlib.colors.to_hex("limegreen"), lr: matplotlib.colors.to_hex("b"), "": "#836BC6", # Neutral color in H&E images. } @@ -797,13 +792,13 @@ def lr_plot( # Showing continuous gene expression of the LR pair # elif outer_mode == "continuous": - if type(l_cmap) == type(None): + if l_cmap is None: l_cmap = matplotlib.colors.LinearSegmentedColormap.from_list( "lcmap", [(0, 0, 0), (0.5, 0, 0), (0.75, 0, 0), (1, 0, 0)] ) else: l_cmap = check_cmap(l_cmap) - if type(r_cmap) == type(None): + if r_cmap is None: r_cmap = matplotlib.colors.LinearSegmentedColormap.from_list( "rcmap", [(0, 0, 0), (0, 0.5, 0), (0, 0.75, 0), (0, 1, 0)] ) @@ -812,10 +807,10 @@ def lr_plot( gene_plot( adata, - gene_symbols=l, + gene_symbols=ligand, size=outer_size_prop * pt_scale, cmap=l_cmap, - color_bar_label=l, + color_bar_label=ligand, ax=ax, fig=fig, crop=False, @@ -824,10 +819,10 @@ def lr_plot( ) gene_plot( adata, - gene_symbols=r, + gene_symbols=receptor, size=middle_size_prop * pt_scale, cmap=r_cmap, - color_bar_label=r, + color_bar_label=receptor, ax=ax, fig=fig, crop=False, @@ -836,11 +831,9 @@ def lr_plot( ) # Adding the cell type labels # - if type(use_label) != type(None): + if use_label is not None: if use_label in lr_use_labels: - inner_cmap = inner_cmap if type(inner_cmap) != type(None) else "copper" - # adata.obsm[f'{lr}_{use_label}'] = adata.uns['per_lr_results'][ - # lr].loc[adata.obs_names,use_label].values + inner_cmap = inner_cmap if inner_cmap is not None else "copper" lr_result_plot( adata, use_lr=lr, @@ -853,7 +846,7 @@ def lr_plot( **kwargs, ) else: - inner_cmap = inner_cmap if type(inner_cmap) != type(None) else "default" + inner_cmap = inner_cmap if inner_cmap is not None else "default" cluster_plot( adata, use_label=use_label, @@ -870,8 +863,8 @@ def lr_plot( # Adding in labels which show the interactions between signicant spots & # neighbours if show_arrows: - l_expr = adata_full[:, l].X.toarray()[:, 0] - r_expr = adata_full[:, r].X.toarray()[:, 0] + l_expr = adata_full[:, ligand].X.toarray()[:, 0] + r_expr = adata_full[:, receptor].X.toarray()[:, 0] if sig_cci: int_df = adata.uns[f"per_lr_cci_{use_label}"][lr] @@ -912,35 +905,35 @@ def lr_plot( #### from old data structure when only test individual LRs. @_docs_params(spatial_base_plot=doc_spatial_base_plot, het_plot=doc_het_plot) def het_plot( - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - zoom_coord: Optional[float] = None, - crop: Optional[bool] = True, - margin: Optional[bool] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # cci_rank param - use_het: Optional[str] = "het", - contour: bool = False, - step_size: Optional[int] = None, - vmin: float = None, - vmax: float = None, -) -> Optional[AnnData]: + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # cci_rank param + use_het: str | None = "het", + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, +) -> AnnData | None: """\ Allows the visualization of significant cell-cell interaction as the values of dot points or contour in the Spatial @@ -996,22 +989,22 @@ def het_plot( def ccinet_plot( - adata: AnnData, - use_label: str, - lr: str = None, - pos: dict = None, - return_pos: bool = False, - cmap: str = "default", - font_size: int = 12, - node_size_exp: int = 1, - node_size_scaler: int = 1, - min_counts: int = 0, - sig_interactions: bool = True, - fig: matplotlib.figure.Figure = None, - ax: matplotlib.axes.Axes = None, - pad=0.25, - title: str = None, - figsize: tuple = (10, 10), + adata: AnnData, + use_label: str, + lr: str = None, + pos: dict = None, + return_pos: bool = False, + cmap: str = "default", + font_size: int = 12, + node_size_exp: int = 1, + node_size_scaler: int = 1, + min_counts: int = 0, + sig_interactions: bool = True, + fig: matplotlib.figure.Figure = None, + ax: matplotlib.axes.Axes = None, + pad=0.25, + title: str = None, + figsize: tuple = (10, 10), ): """Circular celltype-celltype interaction network based on LR-CCI analysis. The size of the nodes drawn for each cell type indicates the total no. of @@ -1052,7 +1045,8 @@ def ccinet_plot( Returns ------- pos: dict - Dictionary of positions where the nodes are draw if return_pos is True, useful for consistent layouts. + Dictionary of positions where the nodes are draw if return_pos is True, + useful for consistent layouts. """ cmap, cmap_n = get_cmap(cmap) # Making sure adata in correct state that this function should run # @@ -1061,7 +1055,7 @@ def ccinet_plot( "Need to first call st.tl.run_cci with the equivalnt " "use_label to visualise cell-cell interactions." ) - elif type(lr) != type(None) and lr not in adata.uns[f"per_lr_cci_{use_label}"]: + elif lr is not None and lr not in adata.uns[f"per_lr_cci_{use_label}"]: raise Exception( f"{lr} not found in {f'per_lr_cci_{use_label}'}, " "suggesting no significant interactions." @@ -1084,7 +1078,7 @@ def ccinet_plot( graph.add_edge(cell_A, cell_B, weight=count) # Determining graph layout, node sizes, & edge colours # - if type(pos) == type(None): + if pos is None: pos = nx.circular_layout(graph) # position the nodes using the layout total = sum(sum(int_matrix)) node_names = list(graph.nodes.keys()) @@ -1092,9 +1086,10 @@ def ccinet_plot( node_sizes = np.array( [ ( - ((sum(int_matrix[i, :] + int_matrix[:, i]) - int_matrix[i, i]) / total) - * 10000 - * node_size_scaler + ((sum(int_matrix[i, :] + int_matrix[:, i]) - int_matrix[ + i, i]) / total) + * 10000 + * node_size_scaler ) ** (node_size_exp) for i in node_indices @@ -1108,8 +1103,8 @@ def ccinet_plot( trans_i = np.where(all_set == edge[0][0])[0][0] receive_i = np.where(all_set == edge[0][1])[0][0] e_total = ( - sum(list(int_matrix[trans_i, :]) + list(int_matrix[:, receive_i])) - - int_matrix[trans_i, receive_i] + sum(list(int_matrix[trans_i, :]) + list(int_matrix[:, receive_i])) + - int_matrix[trans_i, receive_i] ) # so don't double count e_totals.append(e_total) edge_weights = [edge[1]["weight"] / e_totals[i] for i, edge in enumerate(edges)] @@ -1122,7 +1117,7 @@ def ccinet_plot( node_colors = np.array(node_colors)[nodes_indices] #### Drawing the graph ##### - if type(fig) == type(None) or type(ax) == type(None): + if fig is None or ax is None: fig, ax = plt.subplots(figsize=figsize, facecolor=[0.7, 0.7, 0.7, 0.4]) # Adding in the self-loops # @@ -1176,15 +1171,15 @@ def ccinet_plot( def cci_map( - adata: AnnData, - use_label: str, - lr: str = None, - ax: matplotlib.figure.Axes = None, - show: bool = False, - figsize: tuple = None, - cmap: str = "Spectral_r", - sig_interactions: bool = True, - title=None, + adata: AnnData, + use_label: str, + lr: str = None, + ax: matplotlib.figure.Axes = None, + show: bool = False, + figsize: tuple = None, + cmap: str = "Spectral_r", + sig_interactions: bool = True, + title=None, ): """Heatmap visualising sender->receivers of cell type interactions. @@ -1222,7 +1217,7 @@ def cci_map( # Either plotting overall interactions, or just for a particular LR # int_df, title = get_int_df(adata, lr, use_label, sig_interactions, title) - if type(figsize) == type(None): # Adjust size depending on no. cell types + if figsize is None: # Adjust size depending on no. cell types add = np.array([int_df.shape[0] * 0.1, int_df.shape[0] * 0.05]) figsize = tuple(np.array([6.4, 4.8]) + add) @@ -1255,18 +1250,18 @@ def cci_map( def lr_cci_map( - adata: AnnData, - use_label: str, - lrs: list or np.array = None, - n_top_lrs: int = 5, - n_top_ccis: int = 15, - min_total: int = 0, - ax: matplotlib.figure.Axes = None, - figsize: tuple = (6.48, 4.8), - show: bool = False, - cmap: str = "Spectral_r", - square_scaler: int = 700, - sig_interactions: bool = True, + adata: AnnData, + use_label: str, + lrs: list or np.array = None, + n_top_lrs: int = 5, + n_top_ccis: int = 15, + min_total: int = 0, + ax: matplotlib.figure.Axes = None, + figsize: tuple = (6.48, 4.8), + show: bool = False, + cmap: str = "Spectral_r", + square_scaler: int = 700, + sig_interactions: bool = True, ): """Heatmap of interaction counts. Rows are lrs and columns are celltype->celltype interactions. @@ -1310,7 +1305,7 @@ def lr_cci_map( else: lr_int_dfs = adata.uns[f"per_lr_cci_raw_{use_label}"] - if type(lrs) == type(None): + if lrs is None: lrs = np.array(list(lr_int_dfs.keys())) else: lrs = np.array(lrs) @@ -1373,18 +1368,18 @@ def lr_cci_map( def lr_chord_plot( - adata: AnnData, - use_label: str, - lr: str = None, - min_ints: int = 2, - n_top_ccis: int = 10, - cmap: str = "default", - sig_interactions: bool = True, - label_size: int = 10, - label_rotation: float = 0, - title: str = None, - figsize: tuple = (8, 8), - show: bool = True, + adata: AnnData, + use_label: str, + lr: str = None, + min_ints: int = 2, + n_top_ccis: int = 10, + cmap: str = "default", + sig_interactions: bool = True, + label_size: int = 10, + label_rotation: float = 0, + title: str = None, + figsize: tuple = (8, 8), + show: bool = True, ): """Chord diagram of interactions between cell types. Note that interaction is measured as the total no. of edges connecting @@ -1396,8 +1391,8 @@ def lr_chord_plot( Each cell type has a labelled edge taking up a proportion of the outter circle. Chords connecting cell type edges are coloured by the dominant sending cell. Each chord linking cell types has an assymetric shape. - For two cell types, A and B, the side of the chord attached to edge A is sized by - the total interactions from B->A, where B is expressing the ligand & A + For two cell types, A and B, the side of the chord attached to edge A is + sized by the total interactions from B->A, where B is expressing the ligand & A is expressing the receptor. Hence, the proportion of a cell type's edge in the chordplot circle represents the total input signals to that cell type; while the @@ -1419,7 +1414,8 @@ def lr_chord_plot( n_top_ccis: int Maximum no. of CCIs to show, will take the top number of these to display. cmap: str - Cmap to use to get colors if colors not already in adata.uns[f'{use_label}_colors'] + Cmap to use to get colors if colors not already in + adata.uns[f'{use_label}_colors'] sig_interactions: bool Whether to show only significant CCIs or all interaction counts. label_size: str @@ -1455,7 +1451,7 @@ def lr_chord_plot( all_zero = np.array( [np.all(np.logical_and(flux[i, keep] == 0, flux[keep, i] == 0)) for i in keep] ) - keep = keep[all_zero == False] + keep = keep[not all_zero] if len(keep) == 0: # If we don't keep anything, warn the user print( f"Warning: for {lr} at the current min_ints ({min_ints}), there " @@ -1491,7 +1487,7 @@ def lr_chord_plot( rotation = nodePos[i][2] # Prevent text going upside down at certain rotations if (rotation < 90 and rotation > 18 and label_rotation != 0) or ( - rotation < 120 and rotation > 90 + rotation < 120 and rotation > 90 ): label_rotation_ = -label_rotation else: @@ -1507,15 +1503,16 @@ def lr_chord_plot( def grid_plot( - adata, - use_label: str = None, - n_row: int = 10, - n_col: int = 10, - size: int = 1, - figsize=(4.5, 4.5), - show: bool = False, + adata, + use_label: str = None, + n_row: int = 10, + n_col: int = 10, + size: int = 1, + figsize=(4.5, 4.5), + show: bool = False, ): - """Plots grid over the top of spatial data to show how cells will be grouped if gridded. + """Plots grid over the top of spatial data to show how cells will be grouped if + gridded. Parameters ---------- @@ -1544,7 +1541,7 @@ def grid_plot( fig, ax = plt.subplots(figsize=figsize) # Plotting the points # - if type(use_label) != type(None): + if use_label is not None: if f"{use_label}_colors" in adata.uns: color_map = {} for i, ct in enumerate(adata.obs[use_label].cat.categories): @@ -1590,7 +1587,6 @@ def spatialcci_plot_interactive(adata: AnnData): output_notebook() show(bokeh_object.app, notebook_handle=True) - # def het_plot_interactive(adata: AnnData): # bokeh_object = BokehCciPlot(adata) # output_notebook() diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index 6753e44b..1b7b456c 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -1,46 +1,42 @@ """Helper functions for cci_plot.py.""" -import sys -import math -import numpy as np -import pandas as pd import matplotlib +import matplotlib.cm as cm +import matplotlib.colors as plt_colors +import matplotlib.patches as patches import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from anndata import AnnData from matplotlib.axes import Axes from matplotlib.patches import Arc, Wedge -from mpl_toolkits.axes_grid1 import make_axes_locatable - from matplotlib.path import Path -import matplotlib.patches as patches -import matplotlib.colors as plt_colors -import matplotlib.cm as cm +from mpl_toolkits.axes_grid1 import make_axes_locatable from ..tools.microenv.cci.het import get_edges -from anndata import AnnData - # Helper functions for overview plots of the LRs. def lr_scatter( - data, - feature, - highlight_lrs=None, - show_text=True, - n_top=50, - color="gold", - alpha=0.5, - lr_text_fp=None, - axis_text_fp=None, - ax=None, - show=True, - max_text=100, - highlight_color="red", - figsize: tuple = None, - show_all: bool = False, + data, + feature, + highlight_lrs=None, + show_text=True, + n_top=50, + color="gold", + alpha=0.5, + lr_text_fp=None, + axis_text_fp=None, + ax=None, + show=True, + max_text=100, + highlight_color="red", + figsize: tuple = None, + show_all: bool = False, ): """General plotting of the LR features.""" - highlight = type(highlight_lrs) != type(None) + highlight = highlight_lrs is not None if not highlight: show_text = show_text if n_top <= max_text else False else: @@ -112,40 +108,40 @@ def lr_scatter( def rank_scatter( - items, - y, - y_label: str = "", - x_label: str = "", - highlight_items=None, - show_text=True, - color="gold", - alpha=0.5, - lr_text_fp=None, - axis_text_fp=None, - ax=None, - show=True, - highlight_color="red", - rot: float = 90, - point_sizes: np.array = None, - pad=0.2, - figsize=None, - width_ratio=7.5 / 50, - height=4, - point_size_name="Sizes", - point_size_exp=2, - show_all: bool = False, + items, + y, + y_label: str = "", + x_label: str = "", + highlight_items=None, + show_text=True, + color="gold", + alpha=0.5, + lr_text_fp=None, + axis_text_fp=None, + ax=None, + show=True, + highlight_color="red", + rot: float = 90, + point_sizes: np.array = None, + pad=0.2, + figsize=None, + width_ratio=7.5 / 50, + height=4, + point_size_name="Sizes", + point_size_exp=2, + show_all: bool = False, ): """General plotting function for showing ranked list of items.""" ranks = np.array(list(range(len(items)))) - highlight = type(highlight_items) != type(None) - if type(lr_text_fp) == type(None): + highlight = highlight_items is not None + if lr_text_fp is None: lr_text_fp = {"weight": "bold", "size": 8} - if type(axis_text_fp) == type(None): + if axis_text_fp is None: axis_text_fp = {"weight": "bold", "size": 12} - if type(ax) == type(None): - if type(figsize) == type(None): + if ax is None: + if figsize is None: width = width_ratio * len(ranks) if show_text and not highlight else 7.5 if width > 20: width = 20 @@ -158,28 +154,28 @@ def rank_scatter( y, alpha=alpha, c=color, - s=None if type(point_sizes) == type(None) else point_sizes**point_size_exp, + s=None if point_sizes is None else point_sizes ** point_size_exp, edgecolors="none", ) y_min, y_max = ax.get_ylim() y_max = y_max + y_max * pad ax.set_ylim(y_min, y_max) - if type(point_sizes) != type(None): + if point_sizes is not None: # produce a legend with a cross section of sizes from the scatter handles, labels = scatter.legend_elements(prop="sizes", alpha=0.6, num=4) [handle.set_markeredgecolor("none") for handle in handles] starts = [label.find("{") for label in labels] ends = [label.find("}") + 1 for label in labels] sizes = [ - float(label[(starts[i] + 1) : (ends[i] - 1)]) + float(label[(starts[i] + 1): (ends[i] - 1)]) for i, label in enumerate(labels) ] counts = [int(size ** (1 / point_size_exp)) for size in sizes] labels2 = [ - label.replace(label[(starts[i]) : (ends[i])], "{" + str(counts[i]) + "}") + label.replace(label[(starts[i]): (ends[i])], "{" + str(counts[i]) + "}") for i, label in enumerate(labels) ] - legend2 = ax.legend( + ax.legend( handles, labels2, frameon=False, @@ -199,7 +195,7 @@ def rank_scatter( c=highlight_color, s=( None - if type(point_sizes) == type(None) + if point_sizes is None else (point_sizes[ranks_] ** point_size_exp) ), edgecolors=color, @@ -224,19 +220,19 @@ def rank_scatter( def add_arrows( - adata: AnnData, - l_expr: np.array, - r_expr: np.array, - min_expr: float, - sig_bool: np.array, - fig, - ax: Axes, - use_label: str, - int_df: pd.DataFrame, - head_width=4, - width=0.001, - arrow_cmap=None, - arrow_vmax=None, + adata: AnnData, + l_expr: np.array, + r_expr: np.array, + min_expr: float, + sig_bool: np.array, + fig, + ax: Axes, + use_label: str, + int_df: pd.DataFrame, + head_width=4, + width=0.001, + arrow_cmap=None, + arrow_vmax=None, ): """ Adds arrows to the current plot for significant spots to neighbours \ which is interacting with. @@ -270,7 +266,7 @@ def add_arrows( forward_edges, reverse_edges = get_edges(adata, L_bool, R_bool, sig_bool) # If int_df specified, means need to subset to CCIs which are significant # - if type(int_df) != type(None): + if int_df is not None: spot_bcs = adata.obs_names.values.astype(str) spot_labels = adata.obs[use_label].values.astype(str) label_set = int_df.index.values.astype(str) @@ -302,7 +298,7 @@ def add_arrows( forward_edges, reverse_edges = edges_sub # If cmap specified, colour arrows by average LR expression on edge # - if type(arrow_cmap) != type(None): + if arrow_cmap is not None: edges_means = [[], []] all_means = [] for i, edges in enumerate([forward_edges, reverse_edges]): @@ -318,7 +314,7 @@ def add_arrows( all_means.append(mean_expr) # Determining the color maps # - arrow_vmax = np.max(all_means) if type(arrow_vmax) == type(None) else arrow_vmax + arrow_vmax = np.max(all_means) if arrow_vmax is None else arrow_vmax cmap = plt.get_cmap(arrow_cmap) c_norm = plt_colors.Normalize(vmin=0, vmax=arrow_vmax) scalar_map = cm.ScalarMappable(norm=c_norm, cmap=cmap) @@ -365,22 +361,22 @@ def add_arrows( edge_colors=edges_colors[1], ) # Adding the color map # - if type(arrow_cmap) != type(None): - cb1 = matplotlib.colorbar.ColorbarBase( + if arrow_cmap is not None: + matplotlib.colorbar.ColorbarBase( axc, cmap=cmap, norm=c_norm, orientation="horizontal" ) def add_arrows_by_edges( - ax, - adata, - edges, - scale_factor, - head_width, - width, - forward=True, - edge_colors=None, - axc=None, + ax, + adata, + edges, + scale_factor, + head_width, + width, + forward=True, + edge_colors=None, + axc=None, ): """Adds the arrows using an edge list.""" for i, edge in enumerate(edges): @@ -398,7 +394,7 @@ def add_arrows_by_edges( x1, y1 = adata.obsm["spatial"][edge0_index, :] * scale_factor x2, y2 = adata.obsm["spatial"][edge1_index, :] * scale_factor dx, dy = (x2 - x1) * 0.75, (y2 - y1) * 0.75 - arrow_color = "k" if type(edge_colors) == type(None) else edge_colors[i] + arrow_color = "k" if edge_colors is None else edge_colors[i] ax.arrow( x1, @@ -417,9 +413,9 @@ def add_arrows_by_edges( def get_int_df(adata, lr, use_label, sig_interactions, title): """Retrieves the relevant interaction count matrix.""" - no_title = type(title) == type(None) + no_title = title is None labels_ordered = adata.obs[use_label].cat.categories - if type(lr) == type(None): # No LR inputted, so just use all + if lr is None: # No LR inputted, so just use all int_df = ( adata.uns[f"lr_cci_{use_label}"] if sig_interactions @@ -455,10 +451,9 @@ def create_flat_df(int_df): def _box_map(x, y, size, ax=None, figsize=(6.48, 4.8), cmap=None, square_scaler=700): """Main underlying helper function for generating the heatmaps.""" - if type(cmap) == type(None): + if cmap is None: cmap = "Spectral_r" - - if type(ax) == type(None): + if ax is None: fig, ax = plt.subplots(figsize=figsize) # Mapping from column names to integer coordinates @@ -504,11 +499,11 @@ def polar2xy(r, theta): def hex2rgb(c): - return tuple(int(c[i : i + 2], 16) / 256.0 for i in (1, 3, 5)) + return tuple(int(c[i: i + 2], 16) / 256.0 for i in (1, 3, 5)) def IdeogramArc( - start=0, end=60, radius=1.0, width=0.2, ax=None, color=(1, 0, 0), curve_steps=1 + start=0, end=60, radius=1.0, width=0.2, ax=None, color=(1, 0, 0), curve_steps=1 ): # start, end should be in [0, 360) if start > end: @@ -548,25 +543,25 @@ def IdeogramArc( for i in range(1, curve_steps + 1) ] verts_inner = ( - verts_inner_start - + verts_inner_curve - + [polar2xy(inner, start), polar2xy(radius, start)] + verts_inner_start + + verts_inner_curve + + [polar2xy(inner, start), polar2xy(radius, start)] ) verts = verts_upper + verts_inner codes = ( - [Path.MOVETO] - + [Path.CURVE4] * curve_steps * 2 - + [Path.CURVE4, Path.LINETO] - + [Path.CURVE4] * curve_steps * 2 - + [ - Path.CURVE4, - Path.CLOSEPOLY, - ] + [Path.MOVETO] + + [Path.CURVE4] * curve_steps * 2 + + [Path.CURVE4, Path.LINETO] + + [Path.CURVE4] * curve_steps * 2 + + [ + Path.CURVE4, + Path.CLOSEPOLY, + ] ) - if ax == None: + if ax is None: return verts, codes else: path = Path(verts, codes) @@ -577,14 +572,14 @@ def IdeogramArc( def ChordArc( - start1=0, - end1=60, - start2=180, - end2=240, - radius=1.0, - chordwidth=0.7, - ax=None, - color=(1, 0, 0), + start1=0, + end1=60, + start2=180, + end2=240, + radius=1.0, + chordwidth=0.7, + ax=None, + color=(1, 0, 0), ): # start, end should be in [0, 360) if start1 > end1: @@ -630,7 +625,7 @@ def ChordArc( Path.CURVE4, ] - if ax == None: + if ax is None: return verts, codes else: path = Path(verts, codes) @@ -668,7 +663,7 @@ def selfChordArc(start=0, end=60, radius=1.0, chordwidth=0.7, ax=None, color=(1, Path.CURVE4, ] - if ax == None: + if ax is None: return verts, codes else: path = Path(verts, codes) @@ -687,13 +682,15 @@ def chordDiagram(X, ax, colors=None, width=0.1, pad=2, chordwidth=0.7, lim=1.1): ax : matplotlib `axes` to show the plot colors : optional - user defined colors in rgb format. Use function hex2rgb() to convert hex color to rgb color. Default: d3.js category10 + user defined colors in rgb format. Use function hex2rgb() to convert hex + color to rgb color. Default: d3.js category10 width : optional width/thickness of the ideogram arc pad : optional gap pad between two neighboring ideogram arcs, unit: degree, default: 2 degree chordwidth : optional - position of the control points for the chords, controlling the shape of the chords + position of the control points for the chords, controlling the shape + of the chords """ # X[i, j]: i -> j x = X.sum(axis=1) # sum over rows @@ -717,7 +714,7 @@ def chordDiagram(X, ax, colors=None, width=0.1, pad=2, chordwidth=0.7, lim=1.1): ] if len(x) > 10: print("x is too large! Use x smaller than 10") - if type(colors[0]) == str: + if colors[0] is str: colors = [hex2rgb(colors[i]) for i in range(len(x))] # find position for each start and end diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index e60c7a0e..a435cbf3 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -4,60 +4,52 @@ Date: 20 Feb 2021 """ -from lib2to3.pgen2.token import OP -from typing import Optional, Union, Mapping, List # Special -from typing import Sequence, Iterable # ABCs -from typing import Tuple # Classes - import numbers +import warnings +from typing import ( # Special + Optional, # Classes + ) + +import matplotlib +import matplotlib.pyplot as plt +import networkx as nx import numpy as np import pandas as pd from anndata import AnnData - -from matplotlib import rcParams, ticker, gridspec, axes -import matplotlib.pyplot as plt -import matplotlib from scipy.interpolate import griddata -import networkx as nx from ..classes import Spatial -from ..utils import _AxesSubplot, Axes, _read_graph -from .utils import centroidpython, get_cluster, get_node, check_sublist, get_cmap - -################################################################ -# # -# Spatial base plot class # -# # -################################################################ +from ..utils import Axes, _AxesSubplot, _read_graph +from .utils import centroidpython, check_sublist, get_cluster, get_cmap, get_node class SpatialBasePlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - color_bar_label: Optional[str] = "", - zoom_coord: Optional[float] = None, - crop: Optional[bool] = True, - margin: Optional[bool] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 0.7, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - **kwds, + self, + # plotting param + adata: AnnData, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 0.7, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + **kwds, ): super().__init__( adata, @@ -75,13 +67,13 @@ def __init__( if use_raw: self.query_adata = self.adata[0].raw.to_adata().copy() - if self.list_clusters != None: - assert use_label != None, "Please specify `use_label` parameter!" + if self.list_clusters is not None: + assert use_label is not None, "Please specify `use_label` parameter!" - if use_label != None: + if use_label is not None: assert ( - use_label in self.adata[0].obs.columns + use_label in self.adata[0].obs.columns ), "Please choose the right label in `adata.obs.columns`!" self.use_label = use_label @@ -91,7 +83,7 @@ def __init__( self.adata[0].obs[use_label].cat.categories ) else: - if type(self.list_clusters) != list: + if self.list_clusters is not list: self.list_clusters = [self.list_clusters] clusters_indexes = [ @@ -111,21 +103,21 @@ def __init__( stlearn_cmap = ["jana_40", "default"] cmap_available = plt.colormaps() + scanpy_cmap + stlearn_cmap error_msg = ( - "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" - "one of these: " + str(cmap_available) + "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" + "one of these: " + str(cmap_available) ) - if type(cmap) == str: + if cmap is str: assert cmap in cmap_available, error_msg - elif type(cmap) != matplotlib.colors.LinearSegmentedColormap: + elif cmap is not matplotlib.colors.LinearSegmentedColormap: raise Exception(error_msg) self.cmap = cmap - if type(fig) == type(None) and type(ax) == type(None): + if fig is None and ax is None: self.fig, self.ax = self._generate_frame() else: self.fig, self.ax = fig, ax - if show_axis == False: + if not show_axis: self._remove_axis(self.ax) if show_image: @@ -147,7 +139,7 @@ def create_query(list_cl, use_label): if self.list_clusters is not None: # IF not all clusters specified, subset, otherwise just copy. if len(self.list_clusters) != len( - self.adata[0].obs[self.use_label].cat.categories + self.adata[0].obs[self.use_label].cat.categories ): self.query_adata = self.query_adata[ self.query_adata.obs.query( @@ -191,7 +183,7 @@ def _crop_image(self, main_ax: _AxesSubplot, margin: float): main_ax.set_ylim(main_ax.get_ylim()[::-1]) - def _zoom_image(self, main_ax: _AxesSubplot, zoom_coord: Optional[float]): + def _zoom_image(self, main_ax: _AxesSubplot, zoom_coord: float | None): main_ax.set_xlim(zoom_coord[0], zoom_coord[1]) main_ax.set_ylim(zoom_coord[3], zoom_coord[2]) @@ -231,44 +223,41 @@ def _save_output(self): # # ################################################################ -import warnings - - class GenePlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - color_bar_label: Optional[str] = "", - crop: Optional[bool] = True, - zoom_coord: Optional[float] = None, - margin: Optional[bool] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # gene plot param - gene_symbols: Union[str, list] = None, - threshold: Optional[float] = None, - method: str = "CumSum", - contour: bool = False, - step_size: Optional[int] = None, - vmin: float = None, - vmax: float = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # gene plot param + gene_symbols: str | list = None, + threshold: float | None = None, + method: str = "CumSum", + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, + **kwargs, ): super().__init__( adata=adata, @@ -302,9 +291,8 @@ def __init__( self.step_size = step_size - if self.title == None: - if type(gene_symbols) == str: - + if self.title is None: + if gene_symbols is str: self.title = str(gene_symbols) gene_symbols = [gene_symbols] else: @@ -328,7 +316,7 @@ def __init__( if show_color_bar: self._add_color_bar(plot, color_bar_label=color_bar_label) - if fname != None: + if fname is not None: self._save_output() def _get_gene_expression(self): @@ -380,7 +368,7 @@ def _get_gene_expression(self): def _plot_genes(self, gene_values: pd.Series): - if type(self.vmin) == type(None) and type(self.vmax) == type(None): + if self.vmin is None and self.vmax is None: vmin = min(gene_values) vmax = max(gene_values) else: @@ -398,7 +386,7 @@ def _plot_genes(self, gene_values: pd.Series): marker="o", vmin=vmin, vmax=vmax, - cmap=plt.get_cmap(self.cmap) if type(self.cmap) == str else self.cmap, + cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, c=gene_values, ) return plot @@ -417,7 +405,7 @@ def _plot_contour(self, gene_values: pd.Series): yi = np.linspace(y.min(), y.max(), 100) zi = griddata((x, y), z, (xi[None, :], yi[:, None]), method="linear") - if self.step_size == None: + if self.step_size is None: self.step_size = int(np.max(z) / 50) if self.step_size < 1: self.step_size = 1 @@ -428,13 +416,13 @@ def _plot_contour(self, gene_values: pd.Series): yi, zi, range(0, int(np.nanmax(zi)) + self.step_size, self.step_size), - cmap=plt.get_cmap(self.cmap) if type(self.cmap) == str else self.cmap, + cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, alpha=self.cell_alpha, ) return cs def _add_threshold(self, gene_values, threshold): - if threshold == None: + if threshold is None: return np.repeat(True, len(gene_values)) else: return gene_values > threshold @@ -449,38 +437,38 @@ def _add_threshold(self, gene_values, threshold): class FeaturePlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - color_bar_label: Optional[str] = "", - crop: Optional[bool] = True, - zoom_coord: Optional[float] = None, - margin: Optional[bool] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # gene plot param - feature: str = None, - threshold: Optional[float] = None, - contour: bool = False, - step_size: Optional[int] = None, - vmin: float = None, - vmax: float = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # gene plot param + feature: str = None, + threshold: float | None = None, + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, + **kwargs, ): super().__init__( adata=adata, @@ -527,7 +515,7 @@ def __init__( if show_color_bar: self._add_color_bar(plot, color_bar_label=color_bar_label) - if fname != None: + if fname is not None: self._save_output() def _get_feature_values(self): @@ -537,7 +525,7 @@ def _get_feature_values(self): self.feature + " is not in data.obs, please try another feature" ) elif not isinstance( - self.query_adata.obs[self.feature].values[0], numbers.Number + self.query_adata.obs[self.feature].values[0], numbers.Number ): raise ValueError( self.feature @@ -550,7 +538,7 @@ def _get_feature_values(self): def _plot_feature(self, feature_values: pd.Series): - if type(self.vmin) == type(None) and type(self.vmax) == type(None): + if self.vmin is None and self.vmax is None: vmin = min(feature_values) vmax = max(feature_values) else: @@ -568,7 +556,7 @@ def _plot_feature(self, feature_values: pd.Series): marker="o", vmin=vmin, vmax=vmax, - cmap=plt.get_cmap(self.cmap) if type(self.cmap) == str else self.cmap, + cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, c=feature_values, ) return plot @@ -587,7 +575,7 @@ def _plot_contour(self, feature_values: pd.Series): yi = np.linspace(y.min(), y.max(), 100) zi = griddata((x, y), z, (xi[None, :], yi[:, None]), method="linear") - if self.step_size == None: + if self.step_size is None: self.step_size = int(np.max(z) / 50) if self.step_size < 1: self.step_size = 1 @@ -598,65 +586,59 @@ def _plot_contour(self, feature_values: pd.Series): yi, zi, range(0, int(np.nanmax(zi)) + self.step_size, self.step_size), - cmap=plt.get_cmap(self.cmap) if type(self.cmap) == str else self.cmap, + cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, alpha=self.cell_alpha, ) return cs def _add_threshold(self, feature_values, threshold): - if threshold == None: + if threshold is None: return np.repeat(True, len(feature_values)) else: return feature_values > threshold -################################################################ -# # -# Cluster plot class # -# # -################################################################ - - +# Cluster plot class class ClusterPlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "default", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - crop: Optional[bool] = True, - zoom_coord: Optional[float] = None, - margin: Optional[bool] = 100, - size: Optional[float] = 5, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # cluster plot param - show_subcluster: Optional[bool] = False, - show_cluster_labels: Optional[bool] = False, - show_trajectories: Optional[bool] = False, - reverse: Optional[bool] = False, - show_node: Optional[bool] = False, - threshold_spots: Optional[int] = 5, - text_box_size: Optional[float] = 5, - color_bar_size: Optional[float] = 10, - bbox_to_anchor: Optional[Tuple[float, float]] = (1, 1), - # trajectory - trajectory_node_size: Optional[int] = 10, - trajectory_alpha: Optional[float] = 1.0, - trajectory_width: Optional[float] = 2.5, - trajectory_edge_color: Optional[str] = "#f4efd3", - trajectory_arrowsize: Optional[int] = 17, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "default", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 5, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + fname: str | None = None, + dpi: int | None = 120, + # cluster plot param + show_subcluster: bool | None = False, + show_cluster_labels: bool | None = False, + show_trajectories: bool | None = False, + reverse: bool | None = False, + show_node: bool | None = False, + threshold_spots: int | None = 5, + text_box_size: float | None = 5, + color_bar_size: float | None = 10, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + # trajectory + trajectory_node_size: int | None = 10, + trajectory_alpha: float | None = 1.0, + trajectory_width: float | None = 2.5, + trajectory_edge_color: str | None = "#f4efd3", + trajectory_arrowsize: int | None = 17, ): super().__init__( adata=adata, @@ -703,7 +685,6 @@ def __init__( self._add_sub_clusters() if show_trajectories: - self.trajectory_node_size = trajectory_node_size self.trajectory_alpha = trajectory_alpha self.trajectory_width = trajectory_width @@ -712,7 +693,7 @@ def __init__( self._add_trajectories() - if fname != None: + if fname is not None: self._save_output() def _add_cluster_colors(self): @@ -729,7 +710,6 @@ def _add_cluster_colors(self): def _plot_clusters(self): # Plot scatter plot based on pixel of spots - # for i, cluster in enumerate(self.query_adata.obs[self.use_label].cat.categories): for i, cluster in enumerate(self.query_adata.obs.groupby(self.use_label)): # Plot scatter plot based on pixel of spots @@ -783,7 +763,7 @@ def _add_cluster_labels(self): label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ].index + ].index ) subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), label_index) @@ -826,7 +806,7 @@ def _add_sub_clusters(self): label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ].index + ].index ) subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), label_index) @@ -836,18 +816,18 @@ def _add_sub_clusters(self): imgrow_new = subset_spatial[:, 1] * self.scale_factor if ( - len( - self.query_adata.obs[ - self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"].unique() - ) - < 2 + len( + self.query_adata.obs[ + self.query_adata.obs[self.use_label] == str(label) + ]["sub_cluster_labels"].unique() + ) + < 2 ): centroids = [centroidpython(imgcol_new, imgrow_new)] classes = np.array( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"].unique() + ]["sub_cluster_labels"].unique() ) else: @@ -858,7 +838,7 @@ def _add_sub_clusters(self): np.column_stack((imgcol_new, imgrow_new)), self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"], + ]["sub_cluster_labels"], ) centroids = clf.centroids_ @@ -866,12 +846,12 @@ def _add_sub_clusters(self): for j, label in enumerate(classes): if ( - len( - self.query_adata.obs[ - self.query_adata.obs["sub_cluster_labels"] == label - ] - ) - > self.threshold_spots + len( + self.query_adata.obs[ + self.query_adata.obs["sub_cluster_labels"] == label + ] + ) + > self.threshold_spots ): if centroids[j][0] < 1500: x = -100 @@ -973,34 +953,34 @@ def _add_trajectories(self): class SubClusterPlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "jet", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - crop: Optional[bool] = True, - zoom_coord: Optional[float] = None, - margin: Optional[bool] = 100, - size: Optional[float] = 5, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # subcluster plot param - cluster: Optional[int] = 0, - threshold_spots: Optional[int] = 5, - text_box_size: Optional[float] = 5, - bbox_to_anchor: Optional[Tuple[float, float]] = (1, 1), - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "jet", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 5, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + fname: str | None = None, + dpi: int | None = 120, + # subcluster plot param + cluster: int | None = 0, + threshold_spots: int | None = 5, + text_box_size: float | None = 5, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + **kwargs, ): super().__init__( adata=adata, @@ -1032,7 +1012,7 @@ def __init__( self._add_subclusters_label(subset) - if fname != None: + if fname is not None: self._save_output() def _plot_subclusters(self, threshold_spots): @@ -1060,13 +1040,13 @@ def _plot_subclusters(self, threshold_spots): colors = colors.replace(self.mapping) - plot = self.ax.scatter( + self.ax.scatter( self.imgcol_new, self.imgrow_new, edgecolor="none", s=self.size, marker="o", - cmap=plt.get_cmap(self.cmap) if type(self.cmap) == str else self.cmap, + cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, c=colors, alpha=self.cell_alpha, ) @@ -1127,36 +1107,36 @@ def _add_subclusters_label(self, subset): class CciPlot(GenePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - crop: Optional[bool] = True, - zoom_coord: Optional[float] = None, - margin: Optional[bool] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # cci_rank param - use_het: Optional[str] = "het", - contour: bool = False, - step_size: Optional[int] = None, - vmin: float = None, - vmax: float = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # cci_rank param + use_het: str | None = "het", + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, + **kwargs, ): super().__init__( adata=adata, @@ -1196,45 +1176,45 @@ def _get_gene_expression(self): class LrResultPlot(GenePlot): def __init__( - self, - adata: AnnData, - use_lr: Optional["str"] = None, - use_result: Optional["str"] = "lr_sig_scores", - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - crop: Optional[bool] = True, - zoom_coord: Optional[float] = None, - margin: Optional[bool] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # cci_rank param - contour: bool = False, - step_size: Optional[int] = None, - vmin: float = None, - vmax: float = None, - **kwargs, + self, + adata: AnnData, + use_lr: Optional["str"] = None, + use_result: Optional["str"] = "lr_sig_scores", + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # cci_rank param + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, + **kwargs, ): # Making sure cci_rank has been run first # if "lr_summary" not in adata.uns: raise Exception( - f"To visualise LR interaction results, must run" f"st.pl.cci.run first." + "To visualise LR interaction results, must run st.pl.cci.run first." ) # By default, using the LR with most significant spots # - if type(use_lr) == type(None): + if use_lr is None: use_lr = adata.uns["lr_summary"].index.values[0] elif use_lr not in adata.uns["lr_summary"].index: raise Exception( diff --git a/stlearn/plotting/classes_bokeh.py b/stlearn/plotting/classes_bokeh.py index 484d495b..91f678a3 100644 --- a/stlearn/plotting/classes_bokeh.py +++ b/stlearn/plotting/classes_bokeh.py @@ -1,63 +1,58 @@ -from __future__ import division + +from collections import OrderedDict + import numpy as np import pandas as pd -from PIL import Image -from stlearn.tools.microenv.cci.het import get_edges - -from bokeh.plotting import ( - figure, - show, - ColumnDataSource, - curdoc, -) +import scanpy as sc +from anndata import AnnData +from bokeh.application import Application +from bokeh.application.handlers import FunctionHandler +from bokeh.layouts import column, row from bokeh.models import ( + Arrow, + AutocompleteInput, + BasicTicker, BoxSelectTool, - LassoSelectTool, + Button, + CheckboxGroup, + ColorBar, CustomJS, Div, - Paragraph, + HoverTool, + LassoSelectTool, LinearColorMapper, - Slider, + Paragraph, Select, - AutocompleteInput, - ColorBar, + Slider, TextInput, - BasicTicker, - HoverTool, - ZoomOutTool, - CheckboxGroup, - Arrow, VeeHead, - Button, - Dropdown, - Div, + ZoomOutTool, ) - -from bokeh.models.widgets import DataTable, DateFormatter, TableColumn -from anndata import AnnData +from bokeh.models.widgets import DataTable, TableColumn from bokeh.palettes import ( - Spectral11, - Viridis256, - Reds256, Blues256, - Magma256, Category20, + Magma256, + Reds256, + Spectral11, + Viridis256, ) -from bokeh.layouts import column, row, grid -from collections import OrderedDict -from bokeh.application import Application -from bokeh.application.handlers import FunctionHandler +from bokeh.plotting import ( + ColumnDataSource, + figure, +) +from PIL import Image + from stlearn.classes import Spatial -from typing import Optional +from stlearn.tools.microenv.cci.het import get_edges from stlearn.utils import _read_graph -import scanpy as sc class BokehGenePlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__( adata, @@ -327,9 +322,9 @@ def create_violin(self, adata, gene_symbol, use_label): class BokehClusterPlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__(adata) @@ -476,8 +471,8 @@ def __init__( if "rank_genes_groups" in self.adata[0].uns: if ( - self.use_label.value - == self.adata[0].uns["rank_genes_groups"]["params"]["groupby"] + self.use_label.value + == self.adata[0].uns["rank_genes_groups"]["params"]["groupby"] ): self.layout = column(row(self.inputs, self.make_fig()), self.add_dea()) else: @@ -513,13 +508,6 @@ def update_list(self, attrname, old, name): from stlearn.plotting.cluster_plot import cluster_plot cluster_plot(self.adata[0], use_label=self.use_label.value, show_plot=False) - - # self.list_cluster = CheckboxGroup( - # labels=list(self.adata[0].obs[self.use_label.value].cat.categories), - # active=list( - # np.array(range(0, len(self.adata[0].obs[self.use_label.value].unique()))) - # ), - # ) self.list_cluster.labels = list( self.adata[0].obs[self.use_label.value].cat.categories ) @@ -531,8 +519,8 @@ def update_data(self, attrname, old, new): if "rank_genes_groups" in self.adata[0].uns: if ( - self.use_label.value - == self.adata[0].uns["rank_genes_groups"]["params"]["groupby"] + self.use_label.value + == self.adata[0].uns["rank_genes_groups"]["params"]["groupby"] ): self.layout.children[0].children[1] = self.make_fig() self.layout.children[1] = self.add_dea() @@ -776,9 +764,9 @@ def create_dea(self, adata): class BokehLRPlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__( adata, @@ -853,7 +841,6 @@ def __init__( # self.tab = Tabs(tabs = [Panel(child=self.layout, title="Gene plot")]) def modify_fig(doc): - doc.add_root(row(self.layout, width=800)) self.data_alpha.on_change("value", self.update_data) @@ -955,9 +942,9 @@ def _get_lr(self, lr): class BokehSpatialCciPlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__( adata, @@ -1045,7 +1032,6 @@ def __init__( # self.tab = Tabs(tabs = [Panel(child=self.layout, title="Gene plot")]) def modify_fig(doc): - doc.add_root(row(self.layout, width=800)) self.data_alpha.on_change("value", self.update_data) @@ -1165,10 +1151,10 @@ def _get_cci_lr_edges(self): selected = self.annot_select.value # Extracting the data # - l, r = lr.split("_") + ligand, receptor = lr.split("_") lr_index = np.where(adata.uns["lr_summary"].index.values == lr)[0][0] - L_bool = adata[:, l].X.toarray()[:, 0] > 0 - R_bool = adata[:, r].X.toarray()[:, 0] > 0 + L_bool = adata[:, ligand].X.toarray()[:, 0] > 0 + R_bool = adata[:, receptor].X.toarray()[:, 0] > 0 sig_bool = adata.obsm["lr_sig_scores"][:, lr_index] > 0 int_df = adata.uns[f"per_lr_cci_{selected}"][lr] @@ -1225,19 +1211,10 @@ def _add_edges(fig, adata, edges, arrow_size, forward=True, scale_factor=1): ) def update_list(self, attrname, old, name): - # Initialize the color from stlearn.plotting.cluster_plot import cluster_plot - selected = self.annot_select.value.strip("raw_") cluster_plot(self.adata[0], use_label=selected, show_plot=False) - - # self.list_cluster = CheckboxGroup( - # labels=list(self.adata[0].obs[self.use_label.value].cat.categories), - # active=list( - # np.array(range(0, len(self.adata[0].obs[self.use_label.value].unique()))) - # ), - # ) self.list_cluster.labels = list(self.adata[0].obs[selected].cat.categories) self.list_cluster.active = list( np.array(range(0, len(self.adata[0].obs[selected].unique()))) @@ -1246,9 +1223,9 @@ def update_list(self, attrname, old, name): class Annotate(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__(adata) # Open image, and make sure it's RGB*A* @@ -1392,7 +1369,9 @@ def make_fig(self): var new_data = source_data_2.data; - new_data = addRowToAccumulator(new_data,inds,color_index.data.index[0].toString(),color_index.data.index[0]) + ci = color_index.data.index[0]; + cs = ci.toString(); + new_data = addRowToAccumulator(new_data,inds,cs,ci) source_data_2.data = new_data diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index a212a317..84254e82 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -1,66 +1,58 @@ -from matplotlib import pyplot as plt -from PIL import Image -import pandas as pd -import matplotlib -import numpy as np -import networkx as nx - -from typing import Optional, Union, Mapping # Special -from typing import Sequence, Iterable # ABCs -from typing import Tuple # Classes +from typing import ( + Optional, # Special + ) +import matplotlib from anndata import AnnData -import warnings +from bokeh.io import output_notebook +from bokeh.plotting import show +from stlearn.plotting._docs import doc_cluster_plot, doc_spatial_base_plot from stlearn.plotting.classes import ClusterPlot from stlearn.plotting.classes_bokeh import BokehClusterPlot -from stlearn.plotting._docs import doc_spatial_base_plot, doc_cluster_plot -from stlearn.utils import _AxesSubplot, Axes, _docs_params - -from bokeh.io import push_notebook, output_notebook -from bokeh.plotting import show +from stlearn.utils import _docs_params @_docs_params(spatial_base_plot=doc_spatial_base_plot, cluster_plot=doc_cluster_plot) def cluster_plot( - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "default", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - zoom_coord: Optional[float] = None, - crop: Optional[bool] = True, - margin: Optional[bool] = 100, - size: Optional[float] = 5, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # cluster plot param - show_subcluster: Optional[bool] = False, - show_cluster_labels: Optional[bool] = False, - show_trajectories: Optional[bool] = False, - reverse: Optional[bool] = False, - show_node: Optional[bool] = False, - threshold_spots: Optional[int] = 5, - text_box_size: Optional[float] = 5, - color_bar_size: Optional[float] = 10, - bbox_to_anchor: Optional[Tuple[float, float]] = (1, 1), - # trajectory - trajectory_node_size: Optional[int] = 10, - trajectory_alpha: Optional[float] = 1.0, - trajectory_width: Optional[float] = 2.5, - trajectory_edge_color: Optional[str] = "#f4efd3", - trajectory_arrowsize: Optional[int] = 17, -) -> Optional[AnnData]: + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "default", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 5, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + fname: str | None = None, + dpi: int | None = 120, + # cluster plot param + show_subcluster: bool | None = False, + show_cluster_labels: bool | None = False, + show_trajectories: bool | None = False, + reverse: bool | None = False, + show_node: bool | None = False, + threshold_spots: int | None = 5, + text_box_size: float | None = 5, + color_bar_size: float | None = 10, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + # trajectory + trajectory_node_size: int | None = 10, + trajectory_alpha: float | None = 1.0, + trajectory_width: float | None = 2.5, + trajectory_edge_color: str | None = "#f4efd3", + trajectory_arrowsize: int | None = 17, +) -> AnnData | None: """\ Allows the visualization of a cluster results as the discretes values of dot points in the Spatial transcriptomics array. We also support to @@ -81,7 +73,7 @@ def cluster_plot( """ - assert use_label != None, "Please select `use_label` parameter" + assert use_label is not None, "Please select `use_label` parameter" ClusterPlot( adata, @@ -122,9 +114,8 @@ def cluster_plot( def cluster_plot_interactive( - adata: AnnData, + adata: AnnData, ): - bokeh_object = BokehClusterPlot(adata) output_notebook() show(bokeh_object.app, notebook_handle=True) diff --git a/stlearn/plotting/deconvolution_plot.py b/stlearn/plotting/deconvolution_plot.py index 96f83caa..632dfadb 100644 --- a/stlearn/plotting/deconvolution_plot.py +++ b/stlearn/plotting/deconvolution_plot.py @@ -1,40 +1,39 @@ -from typing import Optional, Union -from anndata import AnnData -import matplotlib.pyplot as plt -from matplotlib import cm + import matplotlib as mpl +import matplotlib.pyplot as plt import numpy as np -import stlearn.plotting.utils as utils +from anndata import AnnData def deconvolution_plot( - adata: AnnData, - library_id: str = None, - use_label: str = "louvain", - cluster: [int, str] = None, - celltype: str = None, - celltype_threshold: float = 0, - data_alpha: float = 1.0, - threshold: float = 0.0, - cmap: str = "tab20", - colors: list = None, # The colors to use for each label... - tissue_alpha: float = 1.0, - title: str = None, - spot_size: Union[float, int] = 10, - show_axis: bool = False, - show_legend: bool = True, - show_donut: bool = True, - cropped: bool = True, - margin: int = 100, - name: str = None, - dpi: int = 150, - output: str = None, - copy: bool = False, - figsize: tuple = (6.4, 4.8), - show=True, -) -> Optional[AnnData]: + adata: AnnData, + library_id: str = None, + use_label: str = "louvain", + cluster: [int, str] = None, + celltype: str = None, + celltype_threshold: float = 0, + data_alpha: float = 1.0, + threshold: float = 0.0, + cmap: str = "tab20", + colors: list = None, # The colors to use for each label... + tissue_alpha: float = 1.0, + title: str = None, + spot_size: float | int = 10, + show_axis: bool = False, + show_legend: bool = True, + show_donut: bool = True, + cropped: bool = True, + margin: int = 100, + name: str = None, + dpi: int = 150, + output: str = None, + copy: bool = False, + figsize: tuple = (6.4, 4.8), + show=True, +) -> AnnData | None: """\ - Clustering plot for sptial transcriptomics data. Also it has a function to display trajectory inference. + Clustering plot for sptial transcriptomics data. Also, it has a function to + display trajectory inference. Parameters ---------- @@ -61,7 +60,8 @@ def deconvolution_plot( show_donut Whether to show the donut plot or not. show_trajectory - Show the spatial trajectory or not. It requires stlearn.spatial.trajectory.pseudotimespace. + Show the spatial trajectory or not. It requires + stlearn.spatial.trajectory.pseudotimespace. show_subcluster Show subcluster or not. It requires stlearn.spatial.trajectory.global_level. name @@ -102,7 +102,7 @@ def deconvolution_plot( label_filter_ = label_filter[base.index] - if type(colors) == type(None): + if colors is None: color_vals = list(range(0, len(label_filter_), 1)) my_norm = mpl.colors.Normalize(0, len(label_filter_)) my_cmap = mpl.cm.get_cmap(cmap, len(color_vals)) @@ -143,7 +143,7 @@ def my_autopct(pct): textprops={"fontsize": 5}, ) - if show_legend == True: + if show_legend: ax_cb = fig.add_axes([0.9, 0.25, 0.03, 0.5], axisbelow=False) cb = mpl.colorbar.ColorbarBase( ax_cb, cmap=my_cmap, norm=my_norm, ticks=color_vals diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index 3f3b272a..77b0878c 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -2,59 +2,47 @@ Plotting of continuous features stored in adata.obs. """ -from matplotlib import pyplot as plt -from PIL import Image -import pandas as pd -import matplotlib -import numpy as np - -from typing import Optional, Union, Mapping # Special -from typing import Sequence, Iterable # ABCs -from typing import Tuple # Classes +from typing import ( + Optional, # Special + ) +import matplotlib from anndata import AnnData -import warnings from stlearn.plotting.classes import FeaturePlot -from stlearn.plotting.classes_bokeh import BokehGenePlot -from stlearn.plotting._docs import doc_spatial_base_plot, doc_gene_plot -from stlearn.utils import Empty, _empty, _AxesSubplot, _docs_params - -from bokeh.io import push_notebook, output_notebook -from bokeh.plotting import show # @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) def feat_plot( - adata: AnnData, - feature: str = None, - threshold: Optional[float] = None, - contour: bool = False, - step_size: Optional[int] = None, - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - color_bar_label: Optional[str] = "", - zoom_coord: Optional[float] = None, - crop: Optional[bool] = True, - margin: Optional[bool] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 0.7, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - vmin: Optional[float] = None, - vmax: Optional[float] = None, -) -> Optional[AnnData]: + adata: AnnData, + feature: str = None, + threshold: float | None = None, + contour: bool = False, + step_size: int | None = None, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 0.7, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + vmin: float | None = None, + vmax: float | None = None, +) -> AnnData | None: """\ Allows the visualization of a continuous features stored in adata.obs for Spatial transcriptomics array. diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index c755d12b..860c0b0a 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -1,57 +1,50 @@ -from matplotlib import pyplot as plt -from PIL import Image -import pandas as pd -import matplotlib -import numpy as np - -from typing import Optional, Union, Mapping # Special -from typing import Sequence, Iterable # ABCs -from typing import Tuple # Classes +from typing import ( # Special + Optional, # Classes + ) +import matplotlib from anndata import AnnData -import warnings +from bokeh.io import output_notebook +from bokeh.plotting import show +from stlearn.plotting._docs import doc_gene_plot, doc_spatial_base_plot from stlearn.plotting.classes import GenePlot from stlearn.plotting.classes_bokeh import BokehGenePlot -from stlearn.plotting._docs import doc_spatial_base_plot, doc_gene_plot -from stlearn.utils import Empty, _empty, _AxesSubplot, _docs_params - -from bokeh.io import push_notebook, output_notebook -from bokeh.plotting import show +from stlearn.utils import _docs_params @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) def gene_plot( - adata: AnnData, - gene_symbols: Union[str, list] = None, - threshold: Optional[float] = None, - method: str = "CumSum", - contour: bool = False, - step_size: Optional[int] = None, - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "Spectral_r", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[matplotlib.axes.Axes] = None, - fig: Optional[matplotlib.figure.Figure] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - color_bar_label: Optional[str] = "", - zoom_coord: Optional[float] = None, - crop: Optional[bool] = True, - margin: Optional[bool] = 100, - size: Optional[float] = 7, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 0.7, - use_raw: Optional[bool] = False, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - vmin: Optional[float] = None, - vmax: Optional[float] = None, -) -> Optional[AnnData]: + adata: AnnData, + gene_symbols: str | list = None, + threshold: float | None = None, + method: str = "CumSum", + contour: bool = False, + step_size: int | None = None, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 0.7, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + vmin: float | None = None, + vmax: float | None = None, +) -> AnnData | None: """\ Allows the visualization of a single gene or multiple genes as the values of dot points or contour in the Spatial transcriptomics array. diff --git a/stlearn/plotting/mask_plot.py b/stlearn/plotting/mask_plot.py index 483163a9..6224b8c2 100644 --- a/stlearn/plotting/mask_plot.py +++ b/stlearn/plotting/mask_plot.py @@ -1,26 +1,25 @@ -import matplotlib -from matplotlib import pyplot as plt -from typing import Optional, Union +import matplotlib from anndata import AnnData +from matplotlib import pyplot as plt def plot_mask( - adata: AnnData, - library_id: str = None, - show_spot: bool = True, - spot_alpha: float = 1.0, - cmap: str = "vega_20_scanpy", - tissue_alpha: float = 1.0, - mask_alpha: float = 0.5, - spot_size: Union[float, int] = 6.5, - show_legend: bool = True, - name: str = "mask_plot", - dpi: int = 150, - output: str = None, - show_axis: bool = False, - show_plot: bool = True, -) -> Optional[AnnData]: + adata: AnnData, + library_id: str = None, + show_spot: bool = True, + spot_alpha: float = 1.0, + cmap: str = "vega_20_scanpy", + tissue_alpha: float = 1.0, + mask_alpha: float = 0.5, + spot_size: float | int = 6.5, + show_legend: bool = True, + name: str = "mask_plot", + dpi: int = 150, + output: str = None, + show_axis: bool = False, + show_plot: bool = True, +) -> AnnData | None: """\ mask plot for sptial transcriptomics data. @@ -59,6 +58,7 @@ def plot_mask( Nothing """ from scanpy.plotting import palettes + from stlearn.plotting import palettes_st if cmap == "vega_10_scanpy": @@ -171,5 +171,5 @@ def plot_mask( if output is not None: fig.savefig(output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0) - if show_plot == True: + if show_plot: plt.show() diff --git a/stlearn/plotting/non_spatial_plot.py b/stlearn/plotting/non_spatial_plot.py index e7992d63..e196b8f2 100644 --- a/stlearn/plotting/non_spatial_plot.py +++ b/stlearn/plotting/non_spatial_plot.py @@ -1,22 +1,13 @@ -from matplotlib import pyplot as plt -from PIL import Image -import pandas as pd -import matplotlib -import numpy as np - -from stlearn._compat import Literal -from typing import Optional, Union -from anndata import AnnData -import warnings # from .utils import get_img_from_fig, checkType import scanpy +from anndata import AnnData def non_spatial_plot( - adata: AnnData, - use_label: str = "louvain", -) -> Optional[AnnData]: + adata: AnnData, + use_label: str = "louvain", +) -> AnnData | None: """\ A wrap function to plot all the non-spatial plot from scanpy. diff --git a/stlearn/plotting/stack_3d_plot.py b/stlearn/plotting/stack_3d_plot.py index f229672a..17ee34ff 100644 --- a/stlearn/plotting/stack_3d_plot.py +++ b/stlearn/plotting/stack_3d_plot.py @@ -1,35 +1,40 @@ -from typing import Optional, Union -from anndata import AnnData + import pandas as pd +from anndata import AnnData def stack_3d_plot( - adata: AnnData, - slides, - cmap="viridis", - slide_col="sample_id", - use_label=None, - gene_symbol=None, -) -> Optional[AnnData]: + adata: AnnData, + slides, + height, + width, + cmap="viridis", + slide_col="sample_id", + use_label=None, + gene_symbol=None, +) -> AnnData | None: """\ - Clustering plot for sptial transcriptomics data. Also it has a function to display trajectory inference. + Clustering plot for spatial transcriptomics data. Also, it has a function to + display trajectory inference. Parameters ---------- - adata + adata: Annotated data matrix. - slides + slides: A list of slide id - cmap + width: + Width of the plot + height: + Height of the plot + cmap: Color map - use_label + slide_col: + Obs column to use for coloring. + use_label: Choose label to plot (priotize) gene_symbol Choose gene symbol to plot - width - Witdh of the plot - height - Height of the plot Returns ------- Nothing @@ -40,19 +45,19 @@ def stack_3d_plot( raise ModuleNotFoundError("Please install plotly by `pip install plotly`") assert ( - slide_col in adata.obs.columns + slide_col in adata.obs.columns ), "Please provide the right column for slide_id!" list_df = [] for i, slide in enumerate(slides): - tmp = data.obs[data.obs[slide_col] == slide][["imagecol", "imagerow"]] + tmp = adata.obs[adata.obs[slide_col] == slide][["imagecol", "imagerow"]] tmp["sample_id"] = slide tmp["z-dimension"] = i list_df.append(tmp) df = pd.concat(list_df) - if use_label != None: + if use_label is not None: assert use_label in adata.obs.columns, "Please use the right `use_label`" df[use_label] = adata[df.index].obs[use_label].values diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/plotting/subcluster_plot.py index 3714f6b9..bdca7f54 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/plotting/subcluster_plot.py @@ -1,50 +1,43 @@ -from matplotlib import pyplot as plt -from PIL import Image -import pandas as pd -import matplotlib -import numpy as np - -from typing import Optional, Union, Mapping # Special -from typing import Sequence, Iterable # ABCs -from typing import Tuple # Classes +from typing import ( + Optional, # Special + ) from anndata import AnnData -import warnings -from stlearn.plotting.classes import SubClusterPlot from stlearn.plotting._docs import doc_spatial_base_plot, doc_subcluster_plot -from stlearn.utils import _AxesSubplot, Axes, _docs_params +from stlearn.plotting.classes import SubClusterPlot +from stlearn.utils import _AxesSubplot, _docs_params @_docs_params( spatial_base_plot=doc_spatial_base_plot, subcluster_plot=doc_subcluster_plot ) def subcluster_plot( - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: Optional[Tuple[float, float]] = None, - cmap: Optional[str] = "jet", - use_label: Optional[str] = None, - list_clusters: Optional[list] = None, - ax: Optional[_AxesSubplot] = None, - show_plot: Optional[bool] = True, - show_axis: Optional[bool] = False, - show_image: Optional[bool] = True, - show_color_bar: Optional[bool] = True, - crop: Optional[bool] = True, - margin: Optional[bool] = 100, - size: Optional[float] = 5, - image_alpha: Optional[float] = 1.0, - cell_alpha: Optional[float] = 1.0, - fname: Optional[str] = None, - dpi: Optional[int] = 120, - # subcluster plot param - cluster: Optional[int] = 0, - threshold_spots: Optional[int] = 5, - text_box_size: Optional[float] = 5, - bbox_to_anchor: Optional[Tuple[float, float]] = (1, 1), -) -> Optional[AnnData]: + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "jet", + use_label: str | None = None, + list_clusters: list | None = None, + ax: _AxesSubplot | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 5, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + fname: str | None = None, + dpi: int | None = 120, + # subcluster plot param + cluster: int | None = 0, + threshold_spots: int | None = 5, + text_box_size: float | None = 5, + bbox_to_anchor: tuple[float, float] | None = (1, 1), +) -> AnnData | None: """\ Allows the visualization of a subclustering results as the discretes values of dot points in the Spatial transcriptomics array. @@ -64,9 +57,9 @@ def subcluster_plot( """ - assert use_label != None, "Please select `use_label` parameter" + assert use_label is not None, "Please select `use_label` parameter" assert ( - use_label in adata.obs.columns + use_label in adata.obs.columns ), "Please run `stlearn.spatial.cluster.localization` function!" SubClusterPlot( diff --git a/stlearn/plotting/trajectory/DE_transition_plot.py b/stlearn/plotting/trajectory/DE_transition_plot.py index d988099c..c70bfd6e 100644 --- a/stlearn/plotting/trajectory/DE_transition_plot.py +++ b/stlearn/plotting/trajectory/DE_transition_plot.py @@ -1,17 +1,17 @@ -import matplotlib.pyplot as plt from decimal import Decimal -from typing import Optional, Union + +import matplotlib.pyplot as plt from anndata import AnnData def DE_transition_plot( - adata: AnnData, - top_genes: int = 10, - font_size: int = 6, - name: str = None, - dpi: int = 150, - output: str = None, -) -> Optional[AnnData]: + adata: AnnData, + top_genes: int = 10, + font_size: int = 6, + name: str = None, + dpi: int = 150, + output: str = None, +) -> AnnData | None: """\ Differential expression between transition markers. diff --git a/stlearn/plotting/trajectory/__init__.py b/stlearn/plotting/trajectory/__init__.py index 16681a51..4d5d7457 100644 --- a/stlearn/plotting/trajectory/__init__.py +++ b/stlearn/plotting/trajectory/__init__.py @@ -1,7 +1,17 @@ -from .pseudotime_plot import pseudotime_plot +from .check_trajectory import check_trajectory +from .DE_transition_plot import DE_transition_plot from .local_plot import local_plot -from .tree_plot_simple import tree_plot_simple -from .tree_plot import tree_plot +from .pseudotime_plot import pseudotime_plot from .transition_markers_plot import transition_markers_plot -from .DE_transition_plot import DE_transition_plot -from .check_trajectory import check_trajectory +from .tree_plot import tree_plot +from .tree_plot_simple import tree_plot_simple + +__all__ = [ + "DE_transition_plot", + "check_trajectory", + "local_plot", + "pseudotime_plot", + "transition_markers_plot", + "tree_plot", + "tree_plot_simple", +] diff --git a/stlearn/plotting/trajectory/check_trajectory.py b/stlearn/plotting/trajectory/check_trajectory.py index 29037969..5cab6744 100644 --- a/stlearn/plotting/trajectory/check_trajectory.py +++ b/stlearn/plotting/trajectory/check_trajectory.py @@ -1,35 +1,35 @@ -from anndata import AnnData -from typing import Optional, Union + import matplotlib.pyplot as plt -import scanpy as sc import numpy as np +import scanpy as sc +from anndata import AnnData def check_trajectory( - adata: AnnData, - library_id: str = None, - use_label: str = "louvain", - basis: str = "umap", - pseudotime_key: str = "dpt_pseudotime", - trajectory: list = None, - figsize=(10, 4), - size_umap: int = 50, - size_spatial: int = 1.5, - img_key: str = "hires", -) -> Optional[AnnData]: + adata: AnnData, + library_id: str = None, + use_label: str = "louvain", + basis: str = "umap", + pseudotime_key: str = "dpt_pseudotime", + trajectory: list = None, + figsize=(10, 4), + size_umap: int = 50, + size_spatial: int = 1.5, + img_key: str = "hires", +) -> AnnData | None: trajectory = np.array(trajectory).astype(int) assert ( - trajectory in adata.uns["available_paths"].values() + trajectory in adata.uns["available_paths"].values() ), "Please choose the right path!" trajectory = trajectory.astype(str) assert ( - pseudotime_key in adata.obs.columns + pseudotime_key in adata.obs.columns ), "Please run the pseudotime or choose the right one!" assert ( - use_label in adata.obs.columns + use_label in adata.obs.columns ), "Please run the cluster or choose the right label!" assert basis in adata.obsm, ( - "Please run the " + basis + "before you check the trajectory!" + "Please run the " + basis + "before you check the trajectory!" ) if library_id is None: library_id = list(adata.uns["spatial"].keys())[0] @@ -66,7 +66,7 @@ def check_trajectory( show=False, ) - im = ax2.imshow( + ax2.imshow( adata.uns["spatial"][library_id]["images"][img_key], alpha=0, zorder=-1 ) diff --git a/stlearn/plotting/trajectory/local_plot.py b/stlearn/plotting/trajectory/local_plot.py index a520f8a9..f1c0c7a6 100644 --- a/stlearn/plotting/trajectory/local_plot.py +++ b/stlearn/plotting/trajectory/local_plot.py @@ -1,37 +1,29 @@ -from mpl_toolkits.mplot3d import Axes3D -from matplotlib.patches import FancyArrowPatch -from mpl_toolkits.mplot3d import proj3d + import matplotlib.pyplot as plt import numpy as np -from PIL import Image -import pandas as pd -import matplotlib -import numpy as np -import networkx as nx -from stlearn._compat import Literal -from typing import Optional, Union from anndata import AnnData -import warnings +from matplotlib.patches import FancyArrowPatch +from mpl_toolkits.mplot3d import proj3d def local_plot( - adata: AnnData, - use_label: str = "louvain", - use_cluster: int = None, - reverse: bool = False, - cluster: int = 0, - data_alpha: float = 1.0, - arrow_alpha: float = 1.0, - branch_alpha: float = 0.2, - spot_size: Union[float, int] = 1, - show_color_bar: bool = True, - show_axis: bool = False, - show_plot: bool = True, - name: str = None, - dpi: int = 150, - output: str = None, - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + use_label: str = "louvain", + use_cluster: int = None, + reverse: bool = False, + cluster: int = 0, + data_alpha: float = 1.0, + arrow_alpha: float = 1.0, + branch_alpha: float = 0.2, + spot_size: float | int = 1, + show_color_bar: bool = True, + show_axis: bool = False, + show_plot: bool = True, + name: str = None, + dpi: int = 150, + output: str = None, + copy: bool = False, +) -> AnnData | None: """\ Local spatial trajectory inference plot. @@ -84,8 +76,8 @@ def local_plot( order = 0 for i in ref_cluster.obs["sub_cluster_labels"].unique(): if ( - len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) - > adata.uns["threshold_spots"] + len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) + > adata.uns["threshold_spots"] ): classes_.append(i) centroid_dict = adata.uns["centroid_dict"] @@ -113,9 +105,6 @@ def local_plot( x = np.linspace(centroids_[i][0], centroids_[i + j][0], 1000) z = np.linspace(centroids_[i][1], centroids_[i + j][1], 1000) - branch = ax.plot( - x, y, z, zorder=10, c="#333333", linewidth=1, alpha=branch_alpha - ) if reverse: dpt_distance = -dpt_distance if dpt_distance <= 0: diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index 14c67030..6a92528f 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -1,44 +1,40 @@ -from matplotlib import pyplot as plt -from PIL import Image -import pandas as pd + import matplotlib -import numpy as np import networkx as nx -from stlearn._compat import Literal -from typing import Optional, Union +import numpy as np from anndata import AnnData -import warnings +from matplotlib import pyplot as plt from stlearn.utils import _read_graph def pseudotime_plot( - adata: AnnData, - library_id: str = None, - use_label: str = "louvain", - pseudotime_key: str = "pseudotime_key", - list_clusters: Union[str, list] = None, - cell_alpha: float = 1.0, - image_alpha: float = 1.0, - edge_alpha: float = 0.8, - node_alpha: float = 1.0, - spot_size: Union[float, int] = 6.5, - node_size: float = 5, - show_color_bar: bool = True, - show_axis: bool = False, - show_graph: bool = True, - show_trajectories: bool = False, - reverse: bool = False, - show_node: bool = True, - show_plot: bool = True, - cropped: bool = True, - margin: int = 100, - dpi: int = 150, - output: str = None, - name: str = None, - copy: bool = False, - ax=None, -) -> Optional[AnnData]: + adata: AnnData, + library_id: str = None, + use_label: str = "louvain", + pseudotime_key: str = "pseudotime_key", + list_clusters: str | list = None, + cell_alpha: float = 1.0, + image_alpha: float = 1.0, + edge_alpha: float = 0.8, + node_alpha: float = 1.0, + spot_size: float | int = 6.5, + node_size: float = 5, + show_color_bar: bool = True, + show_axis: bool = False, + show_graph: bool = True, + show_trajectories: bool = False, + reverse: bool = False, + show_node: bool = True, + show_plot: bool = True, + cropped: bool = True, + margin: int = 100, + dpi: int = 150, + output: str = None, + name: str = None, + copy: bool = False, + ax=None, +) -> AnnData | None: """\ Global trajectory inference plot (Only DPT). @@ -87,20 +83,12 @@ def pseudotime_plot( Nothing """ - # plt.rcParams['figure.dpi'] = dpi - imagecol = adata.obs["imagecol"] imagerow = adata.obs["imagerow"] - - if list_clusters == None: + if list_clusters is None: list_clusters = np.array(range(0, len(adata.obs[use_label].unique()))).astype( int ) - # Get query clusters - command = [] - # for i in list_clusters: - # command.append(use_label + ' == "' + str(i) + '"') - # tmp = adata.obs.query(" or ".join(command)) tmp = adata.obs G = _read_graph(adata, "global_graph") @@ -120,13 +108,11 @@ def pseudotime_plot( result2.append(labels[edge[::-1]] + 0.5) fig, a = plt.subplots() - if ax != None: + if ax is not None: a = ax centroid_dict = adata.uns["centroid_dict"] centroid_dict = {int(key): centroid_dict[key] for key in centroid_dict} dpt = adata.obs[pseudotime_key] - - colors = adata.obs[use_label].astype(int) vmin = min(dpt) vmax = max(dpt) # Plot scatter plot based on pixel of spots @@ -149,19 +135,9 @@ def pseudotime_plot( cmap=plt.get_cmap("viridis"), c=scale.reshape(1, -1)[0], ) - - n_clus = len(colors.unique()) - used_colors = adata.uns[use_label + "_colors"] cmaps = matplotlib.colors.LinearSegmentedColormap.from_list("", used_colors) - cmap = plt.get_cmap(cmaps) - bounds = np.linspace(0, n_clus, n_clus + 1) - norm = matplotlib.colors.BoundaryNorm(bounds, cmap.N) - - norm = matplotlib.colors.Normalize(vmin=min(colors), vmax=max(colors)) - m = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap) - if show_graph: nx.draw_networkx( G, @@ -290,7 +266,7 @@ def pseudotime_plot( if output is not None: fig.savefig(output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0) - if show_plot == True: + if show_plot: plt.show() diff --git a/stlearn/plotting/trajectory/transition_markers_plot.py b/stlearn/plotting/trajectory/transition_markers_plot.py index b68ec7fe..267a6e64 100644 --- a/stlearn/plotting/trajectory/transition_markers_plot.py +++ b/stlearn/plotting/trajectory/transition_markers_plot.py @@ -1,17 +1,17 @@ -import matplotlib.pyplot as plt from decimal import Decimal + +import matplotlib.pyplot as plt from anndata import AnnData -from typing import Optional, Union def transition_markers_plot( - adata: AnnData, - top_genes: int = 10, - trajectory: str = None, - dpi: int = 150, - output: str = None, - name: str = None, -) -> Optional[AnnData]: + adata: AnnData, + top_genes: int = 10, + trajectory: str = None, + dpi: int = 150, + output: str = None, + name: str = None, +) -> AnnData | None: """\ Plot transition marker. @@ -34,7 +34,7 @@ def transition_markers_plot( Anndata """ - if trajectory == None: + if trajectory is None: raise ValueError("Please input the trajectory name!") if trajectory not in adata.uns: raise ValueError("Please input the right trajectory name!") diff --git a/stlearn/plotting/trajectory/tree_plot.py b/stlearn/plotting/trajectory/tree_plot.py index 90ade45f..c7999321 100644 --- a/stlearn/plotting/trajectory/tree_plot.py +++ b/stlearn/plotting/trajectory/tree_plot.py @@ -1,38 +1,31 @@ -from matplotlib import pyplot as plt -from PIL import Image -import pandas as pd -import matplotlib -import numpy as np -import networkx as nx import math import random -from stlearn._compat import Literal -from typing import Optional, Union + +import networkx as nx from anndata import AnnData -import warnings -import io -from copy import deepcopy +from matplotlib import pyplot as plt + from stlearn.utils import _read_graph def tree_plot( - adata: AnnData, - library_id: str = None, - figsize: Union[float, int] = (10, 4), - data_alpha: float = 1.0, - use_label: str = "louvain", - spot_size: Union[float, int] = 50, - fontsize: int = 6, - piesize: float = 0.15, - zoom: float = 0.1, - name: str = None, - output: str = None, - dpi: int = 180, - show_all: bool = False, - show_plot: bool = True, - ncols: int = 4, - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + library_id: str = None, + figsize: float | int = (10, 4), + data_alpha: float = 1.0, + use_label: str = "louvain", + spot_size: float | int = 50, + fontsize: int = 6, + piesize: float = 0.15, + zoom: float = 0.1, + name: str = None, + output: str = None, + dpi: int = 180, + show_all: bool = False, + show_plot: bool = True, + ncols: int = 4, + copy: bool = False, +) -> AnnData | None: """\ Hierarchical tree plot represent for the global spatial trajectory inference. @@ -108,7 +101,7 @@ def tree_plot( output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0 ) - if show_plot == True: + if show_plot: plt.show() @@ -120,23 +113,24 @@ def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5 If the graph is a tree this will return the positions to plot this in a hierarchical layout. - G: the graph (must be a tree) - - root: the root node of current branch - - if the tree is directed and this is not given, - the root will be found and used - - if the tree is directed and this is given, then - the positions will be just for the descendants of this node. - - if the tree is undirected and not given, - then a random choice will be used. - - width: horizontal space allocated for this branch - avoids overlap with other branches - - vert_gap: gap between levels of hierarchy - - vert_loc: vertical location of root - - xcenter: horizontal location of root + G: + the graph (must be a tree) + root: + the root node of current branch + - if the tree is directed and this is not given, + the root will be found and used + - if the tree is directed and this is given, then + the positions will be just for the descendants of this node. + - if the tree is undirected and not given, + then a random choice will be used. + width: + horizontal space allocated for this branch - avoids overlap with other branches + vert_gap: + gap between levels of hierarchy + vert_loc: + vertical location of root + xcenter: + horizontal location of root """ if not nx.is_tree(G): raise TypeError("cannot use hierarchy_pos on a graph that is not a tree") @@ -150,7 +144,8 @@ def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5 root = random.choice(list(G.nodes)) def _hierarchy_pos( - G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, parent=None + G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, + parent=None ): """ see hierarchy_pos docstring for most arguments diff --git a/stlearn/plotting/trajectory/tree_plot_simple.py b/stlearn/plotting/trajectory/tree_plot_simple.py index 3b2395fd..84c0882f 100644 --- a/stlearn/plotting/trajectory/tree_plot_simple.py +++ b/stlearn/plotting/trajectory/tree_plot_simple.py @@ -1,38 +1,31 @@ -from matplotlib import pyplot as plt -from PIL import Image -import pandas as pd -import matplotlib -import numpy as np -import networkx as nx import math import random -from stlearn._compat import Literal -from typing import Optional, Union + +import networkx as nx from anndata import AnnData -import warnings -import io -from copy import deepcopy +from matplotlib import pyplot as plt + from stlearn.utils import _read_graph def tree_plot_simple( - adata: AnnData, - library_id: str = None, - figsize: Union[float, int] = (10, 4), - data_alpha: float = 1.0, - use_label: str = "louvain", - spot_size: Union[float, int] = 50, - fontsize: int = 6, - piesize: float = 0.15, - zoom: float = 0.1, - name: str = None, - output: str = None, - dpi: int = 180, - show_all: bool = False, - show_plot: bool = True, - ncols: int = 4, - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + library_id: str = None, + figsize: float | int = (10, 4), + data_alpha: float = 1.0, + use_label: str = "louvain", + spot_size: float | int = 50, + fontsize: int = 6, + piesize: float = 0.15, + zoom: float = 0.1, + name: str = None, + output: str = None, + dpi: int = 180, + show_all: bool = False, + show_plot: bool = True, + ncols: int = 4, + copy: bool = False, +) -> AnnData | None: """\ Hierarchical tree plot represent for the global spatial trajectory inference. @@ -108,7 +101,7 @@ def tree_plot_simple( output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0 ) - if show_plot == True: + if show_plot: plt.show() @@ -120,23 +113,24 @@ def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5 If the graph is a tree this will return the positions to plot this in a hierarchical layout. - G: the graph (must be a tree) - - root: the root node of current branch - - if the tree is directed and this is not given, - the root will be found and used - - if the tree is directed and this is given, then - the positions will be just for the descendants of this node. - - if the tree is undirected and not given, - then a random choice will be used. - - width: horizontal space allocated for this branch - avoids overlap with other branches - - vert_gap: gap between levels of hierarchy - - vert_loc: vertical location of root - - xcenter: horizontal location of root + G: + the graph (must be a tree) + root: + the root node of current branch + - if the tree is directed and this is not given, + the root will be found and used + - if the tree is directed and this is given, then + the positions will be just for the descendants of this node. + - if the tree is undirected and not given, + then a random choice will be used. + width: + horizontal space allocated for this branch - avoids overlap with other branches + vert_gap: + gap between levels of hierarchy + vert_loc: + vertical location of root + xcenter: + horizontal location of root """ if not nx.is_tree(G): raise TypeError("cannot use hierarchy_pos on a graph that is not a tree") @@ -150,7 +144,8 @@ def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5 root = random.choice(list(G.nodes)) def _hierarchy_pos( - G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, parent=None + G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, + parent=None ): """ see hierarchy_pos docstring for most arguments diff --git a/stlearn/plotting/trajectory/utils.py b/stlearn/plotting/trajectory/utils.py index f7b46284..a8095616 100644 --- a/stlearn/plotting/trajectory/utils.py +++ b/stlearn/plotting/trajectory/utils.py @@ -1,5 +1,4 @@ def checkType(arr, n=2): - # If the first two and the last two elements # of the array are in increasing order if arr[0] <= arr[1] and arr[n - 2] <= arr[n - 1]: diff --git a/stlearn/plotting/utils.py b/stlearn/plotting/utils.py index fb22686a..9c5eecf0 100644 --- a/stlearn/plotting/utils.py +++ b/stlearn/plotting/utils.py @@ -1,24 +1,12 @@ -import numpy as np -import pandas as pd - import io -from PIL import Image import matplotlib import matplotlib.pyplot as plt -from anndata import AnnData +import numpy as np +from PIL import Image from scanpy.plotting import palettes -from stlearn.plotting import palettes_st - -from typing import Optional, Union, Mapping # Special -from typing import Sequence, Iterable # ABCs -from typing import Tuple # Classes -from enum import Enum - -from matplotlib import rcParams, ticker, gridspec, axes -from matplotlib.axes import Axes -from abc import ABC +from stlearn.plotting import palettes_st def get_img_from_fig(fig, dpi=180): @@ -39,16 +27,16 @@ def get_img_from_fig(fig, dpi=180): def centroidpython(x, y): - l = len(x) - return sum(x) / l, sum(y) / l + length_of_x = len(x) + return sum(x) / length_of_x, sum(y) / length_of_x def get_cluster(search, dictionary): for ( - cl, - sub, + cl, + sub, ) in ( - dictionary.items() + dictionary.items() ): # for name, age in dictionary.iteritems(): (for Python 2.x) if search in sub: return cl @@ -82,10 +70,10 @@ def get_cmap(cmap): cmap = palettes_st.jana_40 elif cmap == "default": cmap = palettes_st.default - elif type(cmap) == str: # If refers to matplotlib cmap + elif cmap is str: # If refers to matplotlib cmap cmap_n = plt.get_cmap(cmap).N return plt.get_cmap(cmap), cmap_n - elif type(cmap) == matplotlib.colors.LinearSegmentedColormap: # already cmap + elif cmap is matplotlib.colors.LinearSegmentedColormap: # already cmap cmap_n = cmap.N return cmap, cmap_n @@ -103,12 +91,12 @@ def check_cmap(cmap): stlearn_cmap = ["jana_40", "default"] cmap_available = plt.colormaps() + scanpy_cmap + stlearn_cmap error_msg = ( - "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" - "one of these: " + str(cmap_available) + "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" + "one of these: " + str(cmap_available) ) - if type(cmap) == str: + if cmap is str: assert cmap in cmap_available, error_msg - elif type(cmap) != matplotlib.colors.LinearSegmentedColormap: + elif cmap is not matplotlib.colors.LinearSegmentedColormap: raise Exception(error_msg) return cmap @@ -137,7 +125,7 @@ def get_colors(adata, obs_key, cmap="default", label_set=None): adata.uns[col_key] = colors_ordered # Returning the colors of the desired labels in indicated order # - if type(label_set) != type(None): + if label_set is not None: colors_ordered = [ colors_ordered[np.where(labels_ordered == label)[0][0]] for label in label_set diff --git a/stlearn/pp.py b/stlearn/pp.py index 87407591..695efd24 100644 --- a/stlearn/pp.py +++ b/stlearn/pp.py @@ -1,7 +1,16 @@ +from .image_preprocessing.feature_extractor import extract_feature +from .image_preprocessing.image_tiling import tiling from .preprocessing.filter_genes import filter_genes -from .preprocessing.normalize import normalize_total -from .preprocessing.log_scale import log1p -from .preprocessing.log_scale import scale from .preprocessing.graph import neighbors -from .image_preprocessing.image_tiling import tiling -from .image_preprocessing.feature_extractor import extract_feature +from .preprocessing.log_scale import log1p, scale +from .preprocessing.normalize import normalize_total + +__all__ = [ + "filter_genes", + "normalize_total", + "log1p", + "scale", + "neighbors", + "tiling", + "extract_feature", +] diff --git a/stlearn/preprocessing/filter_genes.py b/stlearn/preprocessing/filter_genes.py index 5f102ea6..260773bc 100644 --- a/stlearn/preprocessing/filter_genes.py +++ b/stlearn/preprocessing/filter_genes.py @@ -1,18 +1,17 @@ -from typing import Union, Optional, Tuple, Collection, Sequence, Iterable -from anndata import AnnData + import numpy as np -from scipy.sparse import issparse, isspmatrix_csr, csr_matrix, spmatrix import scanpy +from anndata import AnnData def filter_genes( - adata: AnnData, - min_counts: Optional[int] = None, - min_cells: Optional[int] = None, - max_counts: Optional[int] = None, - max_cells: Optional[int] = None, - inplace: bool = True, -) -> Union[AnnData, None, Tuple[np.ndarray, np.ndarray]]: + adata: AnnData, + min_counts: int | None = None, + min_cells: int | None = None, + max_counts: int | None = None, + max_cells: int | None = None, + inplace: bool = True, +) -> AnnData | None | tuple[np.ndarray, np.ndarray]: """\ Wrap function scanpy.pp.filter_genes diff --git a/stlearn/preprocessing/graph.py b/stlearn/preprocessing/graph.py index 8d7255c1..6d6334dd 100644 --- a/stlearn/preprocessing/graph.py +++ b/stlearn/preprocessing/graph.py @@ -1,12 +1,13 @@ +from collections.abc import Callable, Mapping from types import MappingProxyType -from typing import Union, Optional, Any, Mapping, Callable +from typing import Any import numpy as np -import scipy +import scanpy from anndata import AnnData from numpy.random import RandomState + from .._compat import Literal -import scanpy _Method = Literal["umap", "gauss", "rapids"] _MetricFn = Callable[[np.ndarray, np.ndarray], float] @@ -33,21 +34,21 @@ "sqeuclidean", "yule", ] -_Metric = Union[_MetricSparseCapable, _MetricScipySpatial] +_Metric = _MetricSparseCapable | _MetricScipySpatial def neighbors( - adata: AnnData, - n_neighbors: int = 15, - n_pcs: Optional[int] = None, - use_rep: Optional[str] = None, - knn: bool = True, - random_state: Optional[Union[int, RandomState]] = 0, - method: Optional[_Method] = "umap", - metric: Union[_Metric, _MetricFn] = "euclidean", - metric_kwds: Mapping[str, Any] = MappingProxyType({}), - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + n_neighbors: int = 15, + n_pcs: int | None = None, + use_rep: str | None = None, + knn: bool = True, + random_state: int | RandomState | None = 0, + method: _Method | None = "umap", + metric: _Metric | _MetricFn = "euclidean", + metric_kwds: Mapping[str, Any] = MappingProxyType({}), + copy: bool = False, +) -> AnnData | None: """\ Compute a neighborhood graph of observations [McInnes18]_. The neighbor search efficiency of this heavily relies on UMAP [McInnes18]_, diff --git a/stlearn/preprocessing/log_scale.py b/stlearn/preprocessing/log_scale.py index 8ba18ec2..4fb216d8 100644 --- a/stlearn/preprocessing/log_scale.py +++ b/stlearn/preprocessing/log_scale.py @@ -1,19 +1,17 @@ -from typing import Union, Optional, Tuple, Collection, Sequence, Iterable -from anndata import AnnData + import numpy as np -from scipy.sparse import issparse, isspmatrix_csr, csr_matrix, spmatrix -from scipy import sparse -from stlearn import logging as logg import scanpy +from anndata import AnnData +from scipy.sparse import spmatrix def log1p( - adata: Union[AnnData, np.ndarray, spmatrix], - copy: bool = False, - chunked: bool = False, - chunk_size: Optional[int] = None, - base: Optional[float] = None, -) -> Optional[AnnData]: + adata: AnnData | np.ndarray | spmatrix, + copy: bool = False, + chunked: bool = False, + chunk_size: int | None = None, + base: float | None = None, +) -> AnnData | None: """\ Wrap function of scanpy.pp.log1p Copyright (c) 2017 F. Alexander Wolf, P. Angerer, Theis Lab @@ -47,11 +45,11 @@ def log1p( def scale( - adata: Union[AnnData, np.ndarray, spmatrix], - zero_center: bool = True, - max_value: Optional[float] = None, - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData | np.ndarray | spmatrix, + zero_center: bool = True, + max_value: float | None = None, + copy: bool = False, +) -> AnnData | None: """\ Wrap function of scanpy.pp.scale diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index 0bfe006a..11fd6528 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -1,23 +1,22 @@ -from typing import Optional, Union, Iterable, Dict +from collections.abc import Iterable import numpy as np +import scanpy from anndata import AnnData -from scipy.sparse import issparse -from sklearn.utils import sparsefuncs + from stlearn._compat import Literal -import scanpy def normalize_total( - adata: AnnData, - target_sum: Optional[float] = None, - exclude_highly_expressed: bool = False, - max_fraction: float = 0.05, - key_added: Optional[str] = None, - layers: Union[Literal["all"], Iterable[str]] = None, - layer_norm: Optional[str] = None, - inplace: bool = True, -) -> Optional[Dict[str, np.ndarray]]: + adata: AnnData, + target_sum: float | None = None, + exclude_highly_expressed: bool = False, + max_fraction: float = 0.05, + key_added: str | None = None, + layers: Literal["all"] | Iterable[str] = None, + layer_norm: str | None = None, + inplace: bool = True, +) -> dict[str, np.ndarray] | None: """\ Wrap function from scanpy.pp.log1p Normalize counts per cell. diff --git a/stlearn/spatial.py b/stlearn/spatial.py index 8f62d633..cbf7eced 100644 --- a/stlearn/spatial.py +++ b/stlearn/spatial.py @@ -1,5 +1,9 @@ -from .spatials import clustering -from .spatials import smooth -from .spatials import trajectory -from .spatials import morphology -from .spatials import SME +from .spatials import SME, clustering, morphology, smooth, trajectory + +__all__ = [ + "clustering", + "smooth", + "trajectory", + "morphology", + "SME", +] diff --git a/stlearn/spatials/SME/__init__.py b/stlearn/spatials/SME/__init__.py index 88321427..8fffb497 100644 --- a/stlearn/spatials/SME/__init__.py +++ b/stlearn/spatials/SME/__init__.py @@ -1,2 +1,8 @@ -from .normalize import SME_normalize from .impute import SME_impute0, pseudo_spot +from .normalize import SME_normalize + +__all__ = [ + "SME_normalize", + "SME_impute0", + "pseudo_spot", +] diff --git a/stlearn/spatials/SME/_weighting_matrix.py b/stlearn/spatials/SME/_weighting_matrix.py index 49553763..bd9c0bba 100644 --- a/stlearn/spatials/SME/_weighting_matrix.py +++ b/stlearn/spatials/SME/_weighting_matrix.py @@ -1,10 +1,11 @@ -from sklearn.metrics import pairwise_distances -from typing import Optional, Union -from anndata import AnnData + import numpy as np -from ..._compat import Literal +from anndata import AnnData +from sklearn.metrics import pairwise_distances from tqdm import tqdm +from ..._compat import Literal + _PLATFORM = Literal["Visium", "Old_ST"] _WEIGHTING_MATRIX = Literal[ "weights_matrix_all", @@ -19,13 +20,14 @@ def calculate_weight_matrix( adata: AnnData, - adata_imputed: Union[AnnData, None] = None, + adata_imputed: AnnData | None = None, pseudo_spots: bool = False, platform: _PLATFORM = "Visium", -) -> Optional[AnnData]: - from sklearn.linear_model import LinearRegression +) -> AnnData | None: import math + from sklearn.linear_model import LinearRegression + if platform == "Visium": img_row = adata.obs["imagerow"] img_col = adata.obs["imagecol"] @@ -105,10 +107,10 @@ def calculate_weight_matrix( def impute_neighbour( adata: AnnData, - count_embed: Union[np.ndarray, None] = None, + count_embed: np.ndarray | None = None, weights: _WEIGHTING_MATRIX = "weights_matrix_all", copy: bool = False, -) -> Optional[AnnData]: +) -> AnnData | None: coor = adata.obs[["imagecol", "imagerow"]] weights_matrix = adata.uns[weights] diff --git a/stlearn/spatials/SME/impute.py b/stlearn/spatials/SME/impute.py index 12dcebac..109814bd 100644 --- a/stlearn/spatials/SME/impute.py +++ b/stlearn/spatials/SME/impute.py @@ -1,30 +1,32 @@ -from typing import Optional, Union -from anndata import AnnData from pathlib import Path + import numpy as np -from scipy.sparse import csr_matrix import pandas as pd +import scipy +from anndata import AnnData +from scipy.sparse import csr_matrix + +import stlearn + +from ..._compat import Literal from ._weighting_matrix import ( + _PLATFORM, + _WEIGHTING_MATRIX, calculate_weight_matrix, impute_neighbour, - _WEIGHTING_MATRIX, - _PLATFORM, ) -import stlearn -import scipy -from ..._compat import Literal def SME_impute0( - adata: AnnData, - use_data: str = "raw", - weights: _WEIGHTING_MATRIX = "weights_matrix_all", - platform: _PLATFORM = "Visium", - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + use_data: str = "raw", + weights: _WEIGHTING_MATRIX = "weights_matrix_all", + platform: _PLATFORM = "Visium", + copy: bool = False, +) -> AnnData | None: """\ - using spatial location (S), tissue morphological feature (M) and gene expression (E) information to impute missing - values + using spatial location (S), tissue morphological feature (M) and gene + expression (E) information to impute missing values Parameters ---------- @@ -34,10 +36,10 @@ def SME_impute0( input data, can be `raw` counts or log transformed data weights weighting matrix for imputation. - if `weights_matrix_all`, matrix combined all information from spatial location (S), - tissue morphological feature (M) and gene expression (E) - if `weights_matrix_pd_md`, matrix combined information from spatial location (S), - tissue morphological feature (M) + if `weights_matrix_all`, matrix combined all information from spatial + location (S), tissue morphological feature (M) and gene expression (E) + if `weights_matrix_pd_md`, matrix combined information from spatial + location (S), tissue morphological feature (M) platform `Visium` or `Old_ST` copy @@ -85,24 +87,26 @@ def SME_impute0( def pseudo_spot( - adata: AnnData, - tile_path: Union[Path, str] = Path("/tmp/tiles"), - use_data: str = "raw", - crop_size: int = "auto", - platform: _PLATFORM = "Visium", - weights: _WEIGHTING_MATRIX = "weights_matrix_all", - copy: _COPY = "pseudo_spot_adata", -) -> Optional[AnnData]: + adata: AnnData, + tile_path: Path | str = Path("/tmp/tiles"), + use_data: str = "raw", + crop_size: int = "auto", + platform: _PLATFORM = "Visium", + weights: _WEIGHTING_MATRIX = "weights_matrix_all", + copy: _COPY = "pseudo_spot_adata", +) -> AnnData | None: """\ - using spatial location (S), tissue morphological feature (M) and gene expression (E) information to impute - gap between spots and increase resolution for gene detection + using spatial location (S), tissue morphological feature (M) and gene + expression (E) information to impute gap between spots and increase resolution + for gene detection Parameters ---------- adata Annotated data matrix. use_data - Input data, can be `raw` counts, log transformed data or dimension reduced space(`X_pca` and `X_umap`) + Input data, can be `raw` counts, log transformed data or dimension + reduced space(`X_pca` and `X_umap`) tile_path Path to save spot image tiles crop_size @@ -110,10 +114,10 @@ def pseudo_spot( if `auto`, automatically detect crop size weights Weighting matrix for imputation. - if `weights_matrix_all`, matrix combined all information from spatial location (S), - tissue morphological feature (M) and gene expression (E) - if `weights_matrix_pd_md`, matrix combined information from spatial location (S), - tissue morphological feature (M) + if `weights_matrix_all`, matrix combined all information from spatial + location (S), tissue morphological feature (M) and gene expression (E) + if `weights_matrix_pd_md`, matrix combined information from spatial + location (S), tissue morphological feature (M) platform `Visium` or `Old_ST` copy @@ -124,15 +128,15 @@ def pseudo_spot( ------- Anndata """ - from sklearn.linear_model import LinearRegression import math + from sklearn.linear_model import LinearRegression + if platform == "Visium": img_row = adata.obs["imagerow"] img_col = adata.obs["imagecol"] array_row = adata.obs["array_row"] array_col = adata.obs["array_col"] - rate = 3 obs_df_ = adata.obs[["array_row", "array_col"]].copy() obs_df_.loc[:, "array_row"] = obs_df_["array_row"].apply(lambda x: x - 2 / 3) obs_df = adata.obs[["array_row", "array_col"]].copy() @@ -145,7 +149,6 @@ def pseudo_spot( img_col = adata.obs["imagecol"] array_row = adata.obs_names.map(lambda x: x.split("x")[1]) array_col = adata.obs_names.map(lambda x: x.split("x")[0]) - rate = 1.5 obs_df_left = pd.DataFrame( {"array_row": array_row.to_list(), "array_col": array_col.to_list()}, dtype=np.float64, @@ -246,10 +249,10 @@ def pseudo_spot( reg_col = LinearRegression().fit(array_col.values.reshape(-1, 1), img_col) obs_df.loc[:, "imagerow"] = ( - obs_df.loc[:, "array_row"] * reg_row.coef_ + reg_row.intercept_ + obs_df.loc[:, "array_row"] * reg_row.coef_ + reg_row.intercept_ ) obs_df.loc[:, "imagecol"] = ( - obs_df.loc[:, "array_col"] * reg_col.coef_ + reg_col.intercept_ + obs_df.loc[:, "array_col"] * reg_col.coef_ + reg_col.intercept_ ) impute_coor = obs_df[["imagecol", "imagerow"]] @@ -257,7 +260,7 @@ def pseudo_spot( point_tree = scipy.spatial.cKDTree(coor) n_neighbour = [] - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) + unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) for i in range(len(impute_coor)): current_neighbour = point_tree.query_ball_point( impute_coor.values[i], round(unit) @@ -316,10 +319,10 @@ def pseudo_spot( def _merge( - adata1: AnnData, - adata2: AnnData, - copy: bool = True, -) -> Optional[AnnData]: + adata1: AnnData, + adata2: AnnData, + copy: bool = True, +) -> AnnData | None: merged_df = adata1.to_df().append(adata2.to_df()) merged_df_obs = adata1.obs.append(adata2.obs) merged_adata = AnnData(merged_df, obs=merged_df_obs) diff --git a/stlearn/spatials/SME/normalize.py b/stlearn/spatials/SME/normalize.py index 83b132f9..38849d37 100644 --- a/stlearn/spatials/SME/normalize.py +++ b/stlearn/spatials/SME/normalize.py @@ -1,41 +1,43 @@ -from typing import Optional -from anndata import AnnData + import numpy as np -from scipy.sparse import csr_matrix import pandas as pd +from anndata import AnnData +from scipy.sparse import csr_matrix + from ._weighting_matrix import ( + _PLATFORM, + _WEIGHTING_MATRIX, calculate_weight_matrix, impute_neighbour, - _WEIGHTING_MATRIX, - _PLATFORM, ) def SME_normalize( - adata: AnnData, - use_data: str = "raw", - weights: _WEIGHTING_MATRIX = "weights_matrix_all", - platform: _PLATFORM = "Visium", - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + use_data: str = "raw", + weights: _WEIGHTING_MATRIX = "weights_matrix_all", + platform: _PLATFORM = "Visium", + copy: bool = False, +) -> AnnData | None: """\ - using spatial location (S), tissue morphological feature (M) and gene expression (E) information to normalize data. + using spatial location (S), tissue morphological feature (M) and gene + expression (E) information to normalize data. Parameters ---------- - adata + adata: Annotated data matrix. - use_data + use_data: Input data, can be `raw` counts or log transformed data - weights + weights: Weighting matrix for imputation. - if `weights_matrix_all`, matrix combined all information from spatial location (S), - tissue morphological feature (M) and gene expression (E) - if `weights_matrix_pd_md`, matrix combined information from spatial location (S), - tissue morphological feature (M) - platform + if `weights_matrix_all`, matrix combined all information from spatial + location (S), tissue morphological feature (M) and gene expression (E) + if `weights_matrix_pd_md`, matrix combined information from spatial + location (S), tissue morphological feature (M) + platform: `Visium` or `Old_ST` - copy + copy: Return a copy instead of writing to adata. Returns ------- diff --git a/stlearn/spatials/clustering/__init__.py b/stlearn/spatials/clustering/__init__.py index be151100..7f1e86e7 100644 --- a/stlearn/spatials/clustering/__init__.py +++ b/stlearn/spatials/clustering/__init__.py @@ -1 +1,5 @@ from .localization import localization + +__all__ = [ + "localization", +] diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index 58f83f8f..aaba5d66 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -1,18 +1,18 @@ -from anndata import AnnData -from typing import Optional, Union + import numpy as np import pandas as pd -from sklearn.cluster import DBSCAN +from anndata import AnnData from natsort import natsorted +from sklearn.cluster import DBSCAN def localization( - adata: AnnData, - use_label: str = "louvain", - eps: int = 20, - min_samples: int = 0, - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + use_label: str = "louvain", + eps: int = 20, + min_samples: int = 0, + copy: bool = False, +) -> AnnData | None: """\ Perform local cluster by using DBSCAN. diff --git a/stlearn/spatials/morphology/__init__.py b/stlearn/spatials/morphology/__init__.py index 115a5979..3e5b88f5 100644 --- a/stlearn/spatials/morphology/__init__.py +++ b/stlearn/spatials/morphology/__init__.py @@ -1 +1,5 @@ from .adjust import adjust + +__all__ = [ + "adjust", +] diff --git a/stlearn/spatials/morphology/adjust.py b/stlearn/spatials/morphology/adjust.py index 1128ae1e..039ce5f9 100644 --- a/stlearn/spatials/morphology/adjust.py +++ b/stlearn/spatials/morphology/adjust.py @@ -1,10 +1,11 @@ -from typing import Optional + import numpy as np -from anndata import AnnData -from ..._compat import Literal import scipy.spatial as spatial +from anndata import AnnData from tqdm import tqdm +from ..._compat import Literal + _SIMILARITY_MATRIX = Literal["cosine", "euclidean", "pearson", "spearman"] @@ -16,7 +17,7 @@ def adjust( method="mean", copy: bool = False, similarity_matrix: _SIMILARITY_MATRIX = "cosine", -) -> Optional[AnnData]: +) -> AnnData | None: """\ SME normalisation: Using spot location information and tissue morphological features to correct spot gene expression diff --git a/stlearn/spatials/smooth/__init__.py b/stlearn/spatials/smooth/__init__.py index 70f1149d..3e663461 100644 --- a/stlearn/spatials/smooth/__init__.py +++ b/stlearn/spatials/smooth/__init__.py @@ -1 +1,5 @@ from .disk import disk + +__all__ = [ + "disk", +] diff --git a/stlearn/spatials/smooth/disk.py b/stlearn/spatials/smooth/disk.py index 0517b267..dabf0450 100644 --- a/stlearn/spatials/smooth/disk.py +++ b/stlearn/spatials/smooth/disk.py @@ -1,19 +1,17 @@ -from typing import Optional, Union + import numpy as np -from anndata import AnnData -import logging as logg import scipy.spatial as spatial +from anndata import AnnData def disk( - adata: AnnData, - use_data: str = "X_umap", - radius: float = 10.0, - rates: int = 1, - method: str = "mean", - copy: bool = False, -) -> Optional[AnnData]: - + adata: AnnData, + use_data: str = "X_umap", + radius: float = 10.0, + rates: int = 1, + method: str = "mean", + copy: bool = False, +) -> AnnData | None: coor = adata.obs[["imagecol", "imagerow"]] count_embed = adata.obsm[use_data] point_tree = spatial.cKDTree(coor) @@ -48,7 +46,8 @@ def disk( adata.obsm[new_embed] = np.array(lag_coor) print( - 'Disk smoothing function is applied! The new data are stored in adata.obsm["X_diffmap_disk"]' + 'Disk smoothing function is applied! The new data are stored in ' + + 'adata.obsm["X_diffmap_disk"]' ) return adata if copy else None diff --git a/stlearn/spatials/trajectory/__init__.py b/stlearn/spatials/trajectory/__init__.py index bd6c4820..f2922e65 100644 --- a/stlearn/spatials/trajectory/__init__.py +++ b/stlearn/spatials/trajectory/__init__.py @@ -1,14 +1,30 @@ +from .compare_transitions import compare_transitions +from .detect_transition_markers import ( + detect_transition_markers_branches, + detect_transition_markers_clades, +) from .global_level import global_level from .local_level import local_level from .pseudotime import pseudotime -from .weight_optimization import weight_optimizing_global, weight_optimizing_local -from .utils import lambda_dist, resistance_distance from .pseudotimespace import pseudotimespace_global, pseudotimespace_local -from .detect_transition_markers import ( - detect_transition_markers_clades, - detect_transition_markers_branches, -) -from .compare_transitions import compare_transitions - from .set_root import set_root from .shortest_path_spatial_PAGA import shortest_path_spatial_PAGA +from .utils import lambda_dist, resistance_distance +from .weight_optimization import weight_optimizing_global, weight_optimizing_local + +__all__ = [ + "global_level", + "local_level", + "pseudotime", + "weight_optimizing_global", + "weight_optimizing_local", + "lambda_dist", + "resistance_distance", + "pseudotimespace_global", + "pseudotimespace_local", + "detect_transition_markers_clades", + "detect_transition_markers_branches", + "compare_transitions", + "set_root", + "shortest_path_spatial_PAGA", +] diff --git a/stlearn/spatials/trajectory/detect_transition_markers.py b/stlearn/spatials/trajectory/detect_transition_markers.py index 56497ada..d703894b 100644 --- a/stlearn/spatials/trajectory/detect_transition_markers.py +++ b/stlearn/spatials/trajectory/detect_transition_markers.py @@ -1,20 +1,21 @@ -from scipy.stats import spearmanr +import warnings + import numpy as np import pandas as pd -import warnings -import networkx as nx +from scipy.stats import spearmanr + from ...utils import _read_graph warnings.filterwarnings("ignore", category=RuntimeWarning) def detect_transition_markers_clades( - adata, - clade, - cutoff_spearman=0.4, - cutoff_pvalue=0.05, - screening_genes=None, - use_raw_count=False, + adata, + clade, + cutoff_spearman=0.4, + cutoff_pvalue=0.05, + screening_genes=None, + use_raw_count=False, ): """\ Transition markers detection of a clade. @@ -68,7 +69,7 @@ def detect_transition_markers_clades( ) negative = spearman_result[ spearman_result["score"] <= cutoff_spearman * (-1) - ].sort_values("score") + ].sort_values("score") result = pd.concat([positive, negative]) @@ -80,12 +81,12 @@ def detect_transition_markers_clades( def detect_transition_markers_branches( - adata, - branch, - cutoff_spearman=0.4, - cutoff_pvalue=0.05, - screening_genes=None, - use_raw_count=False, + adata, + branch, + cutoff_spearman=0.4, + cutoff_pvalue=0.05, + screening_genes=None, + use_raw_count=False, ): """\ Transition markers detection of a branch. @@ -125,7 +126,7 @@ def detect_transition_markers_branches( ) negative = spearman_result[ spearman_result["score"] <= cutoff_spearman * (-1) - ].sort_values("score") + ].sort_values("score") result = pd.concat([positive, negative]) @@ -152,7 +153,7 @@ def get_rank_cor(adata, screening_genes=None, use_raw_count=True): tmp = tmp.to_df() else: tmp = adata.to_df() - if screening_genes != None: + if screening_genes is not None: tmp = tmp[screening_genes] dpt = adata.obs["dpt_pseudotime"].values genes = [] diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatials/trajectory/global_level.py index 6a898238..8d7b4493 100644 --- a/stlearn/spatials/trajectory/global_level.py +++ b/stlearn/spatials/trajectory/global_level.py @@ -1,24 +1,23 @@ -from anndata import AnnData -from typing import Optional, Union -import numpy as np -import pandas as pd + import networkx as nx +import numpy as np +from anndata import AnnData from scipy.spatial.distance import cdist + from stlearn.utils import _read_graph -from sklearn.metrics import pairwise_distances def global_level( - adata: AnnData, - use_label: str = "louvain", - use_rep: str = "X_pca", - n_dims: int = 40, - list_clusters: list = [], - return_graph: bool = False, - w: float = None, - verbose: bool = True, - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + use_label: str = "louvain", + use_rep: str = "X_pca", + n_dims: int = 40, + list_clusters: list = [], + return_graph: bool = False, + w: float = None, + verbose: bool = True, + copy: bool = False, +) -> AnnData | None: """\ Perform global sptial trajectory inference. @@ -51,7 +50,7 @@ def global_level( inds_cat = {v: k for (k, v) in cat_inds.items()} # Query cluster - if type(list_clusters[0]) == str: + if list_clusters[0] is str: list_clusters = [cat_inds[label] for label in list_clusters] query_nodes = list_clusters @@ -115,7 +114,8 @@ def global_level( H_sub = H.edge_subgraph(edge_list) if not nx.is_connected(H_sub.to_undirected()): raise ValueError( - "The chosen clusters are not available to construct the spatial trajectory! Please choose other path." + "The chosen clusters are not available to construct the spatial " + + "trajectory! Please choose other path." ) H_sub = nx.DiGraph(H_sub) prepare_root = [] @@ -179,11 +179,7 @@ def global_level( return H_sub -######################## -## Global level PTS ## -######################## - - +# Global level PTS def get_node(node_list, split_node): result = np.array([]) for node in node_list: @@ -201,42 +197,6 @@ def ordering_nodes(node_list, use_label, adata): return list(np.array(node_list)[np.argsort(mean_dpt)]) -# def dpt_distance_matrix(adata, cluster1, cluster2, use_label): -# tmp = adata.obs[adata.obs[use_label] == str(cluster1)] -# chosen_adata1 = adata[list(tmp.index)] -# tmp = adata.obs[adata.obs[use_label] == str(cluster2)] -# chosen_aadata = adata[list(tmp.index)] - -# sub_dpt1 = [] -# chosen_sub1 = chosen_adata1.obs["sub_cluster_labels"].unique() -# for i in range(0, len(chosen_sub1)): -# sub_dpt1.append( -# chosen_adata1.obs[ -# chosen_adata1.obs["sub_cluster_labels"] == chosen_sub1[i] -# ]["dpt_pseudotime"].median() -# ) - -# sub_dpt2 = [] -# chosen_sub2 = chosen_aadata.obs["sub_cluster_labels"].unique() -# for i in range(0, len(chosen_sub2)): -# sub_dpt2.append( -# chosen_aadata.obs[ -# chosen_aadata.obs["sub_cluster_labels"] == chosen_sub2[i] -# ]["dpt_pseudotime"].median() -# ) - -# dm = cdist( -# np.array(sub_dpt1).reshape(-1, 1), -# np.array(sub_dpt2).reshape(-1, 1), -# lambda u, v: v - u, -# ) -# from sklearn.preprocessing import MinMaxScaler -# scaler = MinMaxScaler() -# scale_dm = scaler.fit_transform(dm) -# # scale_dm = (dm + (-np.min(dm))) / np.max(dm) -# return scale_dm - - def spatial_distance_matrix(adata, cluster1, cluster2, use_label): tmp = adata.obs[adata.obs[use_label] == str(cluster1)] chosen_adata1 = adata[list(tmp.index)] @@ -258,8 +218,6 @@ def spatial_distance_matrix(adata, cluster1, cluster2, use_label): sdm = cdist(np.array(sub_coord1), np.array(sub_coord2), "euclidean") - from sklearn.preprocessing import MinMaxScaler - # scaler = MinMaxScaler() # scale_sdm = scaler.fit_transform(sdm) scale_sdm = sdm / np.max(sdm) @@ -304,15 +262,12 @@ def ge_distance_matrix(adata, cluster1, cluster2, use_label, use_rep, n_dims): results.append(cdist(sub_coord1[i], sub_coord2[j], "cosine").mean()) results = np.array(results).reshape(len(sub_coord1), len(sub_coord2)) - from sklearn.preprocessing import MinMaxScaler - # scaler = MinMaxScaler() # scale_sdm = scaler.fit_transform(results) scale_sdm = results / np.max(results) return scale_sdm - # def _density_normalize(other: Union[np.ndarray, spmatrix] # ) -> Union[np.ndarray, spmatrix]: # """ diff --git a/stlearn/spatials/trajectory/local_level.py b/stlearn/spatials/trajectory/local_level.py index 79c0b6e4..a5dacb76 100644 --- a/stlearn/spatials/trajectory/local_level.py +++ b/stlearn/spatials/trajectory/local_level.py @@ -1,20 +1,18 @@ -from anndata import AnnData -from typing import Optional, Union + import numpy as np -from stlearn.em import run_pca, run_diffmap -from stlearn.pp import neighbors +from anndata import AnnData from scipy.spatial.distance import cdist def local_level( - adata: AnnData, - use_label: str = "louvain", - cluster: int = 9, - w: float = 0.5, - return_matrix: bool = False, - verbose: bool = True, - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + use_label: str = "louvain", + cluster: int = 9, + w: float = 0.5, + return_matrix: bool = False, + verbose: bool = True, + copy: bool = False, +) -> AnnData | None: """\ Perform local sptial trajectory inference (required run pseudotime first). @@ -51,8 +49,8 @@ def local_level( centroid_dict = {int(key): centroid_dict[key] for key in centroid_dict} for i in list_cluster: if ( - len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) - > adata.uns["threshold_spots"] + len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) + > adata.uns["threshold_spots"] ): dpt.append( cluster_data.obs[cluster_data.obs["sub_cluster_labels"] == i][ diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index 3710ee61..8ae61b74 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -1,58 +1,57 @@ -from anndata import AnnData -from typing import Optional, Union + +import networkx as nx import numpy as np import pandas as pd -import networkx as nx -from scipy.spatial.distance import cdist import scanpy +from anndata import AnnData def pseudotime( - adata: AnnData, - use_label: str = None, - eps: float = 20, - n_neighbors: int = 25, - use_rep: str = "X_pca", - threshold: float = 0.01, - radius: int = 50, - method: str = "mean", - threshold_spots: int = 5, - use_sme: bool = False, - reverse: bool = False, - pseudotime_key: str = "dpt_pseudotime", - max_nodes: int = 4, - run_knn: bool = False, - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + use_label: str = None, + eps: float = 20, + n_neighbors: int = 25, + use_rep: str = "X_pca", + threshold: float = 0.01, + radius: int = 50, + method: str = "mean", + threshold_spots: int = 5, + use_sme: bool = False, + reverse: bool = False, + pseudotime_key: str = "dpt_pseudotime", + max_nodes: int = 4, + run_knn: bool = False, + copy: bool = False, +) -> AnnData | None: """\ Perform pseudotime analysis. Parameters ---------- - adata + adata: Annotated data matrix. - use_label + use_label: Use label result of cluster method. - eps + eps: The maximum distance between two samples for one to be considered as in the neighborhood of the other. This is not a maximum bound on the distances of points within a cluster. This is the most important DBSCAN parameter to choose appropriately for your data set and distance function. - threshold + threshold: Threshold to find the significant connection for PAGA graph. - radius + radius: radius to adjust data for diffusion map - method + method: method to adjust the data. - use_sme + use_sme: Use adjusted feature by SME normalization or not - reverse + reverse: Reverse the pseudotime score - pseudotime_key + pseudotime_key: Key to store pseudotime - max_nodes + max_nodes: Maximum number of node in available paths - copy + copy: Return a copy instead of writing to adata. Returns ------- @@ -68,7 +67,7 @@ def pseudotime( except: pass - assert use_label != None, "Please choose the right `use_label`!" + assert use_label is not None, "Please choose the right `use_label`!" # Localize from stlearn.spatials.clustering import localization @@ -114,8 +113,8 @@ def pseudotime( "sub_cluster_labels" ].unique(): if ( - len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) - > threshold_spots + len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) + > threshold_spots ): meaningful_sub.append(i) @@ -190,9 +189,7 @@ def closest_node(node, nodes): return adata if copy else None -######## utils ######## - - +# Utils def replace_with_dict(ar, dic): # Extract out keys and values k = np.array(list(dic.keys()), dtype=object) @@ -212,7 +209,6 @@ def selection_sort(x): def store_available_paths(adata, threshold, use_label, max_nodes, pseudotime_key): - # Read original PAGA graph G = nx.from_numpy_array(adata.uns["paga"]["connectivities"].toarray()) edge_weights = nx.get_edge_attributes(G, "weight") @@ -247,7 +243,6 @@ def store_available_paths(adata, threshold, use_label, max_nodes, pseudotime_key adata.uns["available_paths"] = all_paths print( - "All available trajectory paths are stored in adata.uns['available_paths'] with length < " - + str(max_nodes) - + " nodes" + "All available trajectory paths are stored in adata.uns['available_paths'] " + + "with length < " + str(max_nodes) + " nodes" ) diff --git a/stlearn/spatials/trajectory/pseudotimespace.py b/stlearn/spatials/trajectory/pseudotimespace.py index d238a428..7f362c1d 100644 --- a/stlearn/spatials/trajectory/pseudotimespace.py +++ b/stlearn/spatials/trajectory/pseudotimespace.py @@ -1,35 +1,41 @@ + from anndata import AnnData -from typing import Optional, Union -from .weight_optimization import weight_optimizing_global, weight_optimizing_local + from .global_level import global_level from .local_level import local_level +from .weight_optimization import weight_optimizing_global, weight_optimizing_local def pseudotimespace_global( - adata: AnnData, - use_label: str = "louvain", - use_rep: str = "X_pca", - n_dims: int = 40, - list_clusters: list = [], - model: str = "spatial", - step=0.01, - k=10, -) -> Optional[AnnData]: + adata: AnnData, + use_label: str = "louvain", + use_rep: str = "X_pca", + n_dims: int = 40, + list_clusters=None, + model: str = "spatial", + step=0.01, + k=10, +) -> AnnData | None: """\ Perform pseudo-time-space analysis with global level. Parameters ---------- - adata + adata: Annotated data matrix. - use_label + use_label: Use label result of cluster method. - list_clusters + use_rep: + Which obsm location to use. + n_dims: + Number of dimensions to use in PCA + list_clusters: List of cluster used to reconstruct spatial trajectory. - w - Weighting factor to balance between spatial data and gene expression - step - Step for screeing weighting factor + model: + Can be mixed, spatial or gene expression. spatial sets weight to 0, + gene expression sets weight to 1 and mixed uses the list_clusters, step and k. + step: + Step for screening weighting factor k The number of eigenvalues to be compared Returns @@ -37,8 +43,10 @@ def pseudotimespace_global( Anndata """ - if model == "mixed": + if list_clusters is None: + list_clusters = [] + if model == "mixed": w = weight_optimizing_global( adata, use_label=use_label, list_clusters=list_clusters, step=step, k=k ) @@ -47,8 +55,9 @@ def pseudotimespace_global( elif model == "gene_expression": w = 1 else: - raise ValidationError( - "Please choose the right model! Available models: 'mixed', 'spatial' and 'gene_expression' " + raise ValueError( + "Please choose the right model! Available models: 'mixed', 'spatial' " + + "and 'gene_expression' " ) global_level( @@ -62,29 +71,31 @@ def pseudotimespace_global( def pseudotimespace_local( - adata: AnnData, - use_label: str = "louvain", - cluster: list = [], - w: float = None, -) -> Optional[AnnData]: + adata: AnnData, + use_label: str = "louvain", + cluster=None, + w: float = None, +) -> AnnData | None: """\ Perform pseudo-time-space analysis with local level. Parameters ---------- - adata + adata: Annotated data matrix. - use_label + use_label: Use label result of cluster method. - cluster - Cluster used to reconstruct intraregional spatial trajectory. - w + cluster: + Cluster used to reconstruct intra regional spatial trajectory. + w: Weighting factor to balance between spatial data and gene expression Returns ------- Anndata """ + if cluster is None: + cluster = [] if w is None: w = weight_optimizing_local(adata, use_label=use_label, cluster=cluster) diff --git a/stlearn/spatials/trajectory/set_root.py b/stlearn/spatials/trajectory/set_root.py index 0805c0c6..88f8251e 100644 --- a/stlearn/spatials/trajectory/set_root.py +++ b/stlearn/spatials/trajectory/set_root.py @@ -1,6 +1,6 @@ -from anndata import AnnData -from typing import Optional, Union import numpy as np +from anndata import AnnData + from stlearn.spatials.trajectory.utils import _correlation_test_helper @@ -28,9 +28,9 @@ def set_root(adata: AnnData, use_label: str, cluster: str, use_raw: bool = False # Subset the data based on the chosen cluster tmp_adata = tmp_adata[ - tmp_adata.obs[tmp_adata.obs[use_label] == str(cluster)].index, : - ] - if use_raw == True: + tmp_adata.obs[tmp_adata.obs[use_label] == str(cluster)].index, : + ] + if use_raw: tmp_adata = tmp_adata.raw.to_adata() # Borrow from Cellrank to calculate CytoTrace score diff --git a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py b/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py index bfd6b359..7d7ea2ae 100644 --- a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py +++ b/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py @@ -1,5 +1,6 @@ import networkx as nx import numpy as np + from stlearn.utils import _read_graph diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatials/trajectory/utils.py index e7ab2909..8381cc74 100644 --- a/stlearn/spatials/trajectory/utils.py +++ b/stlearn/spatials/trajectory/utils.py @@ -1,9 +1,18 @@ +from collections.abc import Sequence + +import networkx as nx +import numpy as np from numpy import linalg as la +from scipy import linalg as spla +from scipy import sparse as sps +from scipy.sparse import csr_matrix, issparse, isspmatrix_csr, spmatrix +from scipy.sparse import linalg as sparse_spla +from scipy.stats import norm def lambda_dist(A1, A2, k=None, p=2, kind="laplacian"): - """The function is migrated from NetComp package. The lambda distance between graphs, which is defined as - d(G1,G2) = norm(L_1 - L_2) + """The function is migrated from NetComp package. The lambda distance between + graphs, which is defined as d(G1,G2) = norm(L_1 - L_2) where L_1 is a vector of the top k eigenvalues of the appropriate matrix associated with G1, and L2 is defined similarly. Parameters @@ -52,7 +61,7 @@ def lambda_dist(A1, A2, k=None, p=2, kind="laplacian"): def resistance_distance( - A1, A2, p=2, renormalized=False, attributed=False, check_connected=True, beta=1 + A1, A2, p=2, renormalized=False, attributed=False, check_connected=True, beta=1 ): """Compare two graphs using resistance distance (possibly renormalized). Parameters @@ -99,11 +108,11 @@ def resistance_distance( ] try: distance_vector = np.sum((R1 - R2) ** p, axis=1) - except ValueError: - raise InputError( + except ValueError as e: + raise ValueError( "Input matrices are different sizes. Please use " "renormalized resistance distance." - ) + ) from e if attributed: return distance_vector ** (1 / p) else: @@ -114,20 +123,7 @@ def resistance_distance( # Eigenstuff # ********** # Functions for calculating eigenstuff of graphs. - - -from scipy import sparse as sps -import numpy as np -from scipy.sparse import linalg as spla -from numpy import linalg as la - -from scipy.sparse import issparse - -###################### -## Helper Functions ## -###################### - - +# Helper Functions def _eigs(M, which="SR", k=None): """Helper function for getting eigenstuff. Parameters @@ -155,7 +151,7 @@ def _eigs(M, which="SR", k=None): raise ValueError("which must be either 'LR' or 'SR'.") M = M.astype(float) if issparse(M) and k < n - 1: - evals, evecs = spla.eigs(M, k=k, which=which) + evals, evecs = sparse_spla.eigs(M, k=k, which=which) else: try: M = M.todense() @@ -174,11 +170,7 @@ def _eigs(M, which="SR", k=None): return np.real(evals), np.real(evecs) -##################### -## Get Eigenstuff ## -##################### - - +# Get Eigenstuff def normalized_laplacian_eig(A, k=None): """Return the eigenstuff of the normalized Laplacian matrix of graph associated with adjacency matrix A. @@ -213,9 +205,7 @@ def normalized_laplacian_eig(A, k=None): nx.normalized_laplacian_matrix """ n, m = A.shape - ## - ## TODO: implement checks on the adjacency matrix - ## + # TODO: implement checks on the adjacency matrix degs = _flat(A.sum(axis=1)) # the below will break if inv_root_degs = [d ** (-1 / 2) if d > _eps else 0 for d in degs] @@ -234,18 +224,10 @@ def normalized_laplacian_eig(A, k=None): # Matrices associated with graphs. Also contains linear algebraic helper functions. # """ - -from scipy import sparse as sps -from scipy.sparse import issparse -import numpy as np - _eps = 10 ** (-10) # a small parameter -###################### -## Helper Functions ## -###################### - +# Helper Functions def _flat(D): """Flatten column or row matrices, as well as arrays.""" if issparse(D): @@ -274,11 +256,7 @@ def _pad(A, N): return A_pad -######################## -## Matrices of Graphs ## -######################## - - +# Matrices of Graphs def degree_matrix(A): """Diagonal degree matrix of graph with adjacency matrix A Parameters @@ -338,16 +316,6 @@ class UndefinedException(Exception): # Resistance matrix. Renormalized version, as well as conductance and commute matrices. # """ -import networkx as nx -from numpy import linalg as la -from scipy import linalg as spla -import numpy as np -from scipy.sparse import issparse - -# from netcomp.linalg.matrices import laplacian_matrix -# from netcomp.exception import UndefinedException - - def resistance_matrix(A, check_connected=True): """Return the resistance matrix of G. Parameters @@ -543,37 +511,10 @@ def conductance_matrix(A): return C -######################## -## CytoTrace wrapper ## -######################## - -from typing import ( - Any, - Dict, - List, - Tuple, - Union, - TypeVar, - Hashable, - Iterable, - Optional, - Sequence, -) -import numpy as np -import pandas as pd -from pandas import Series -from scipy.stats import norm -from numpy.linalg import norm as d_norm -from scipy.sparse import eye as speye -from scipy.sparse import diags, issparse, spmatrix, csr_matrix, isspmatrix_csr -from sklearn.cluster import KMeans -from pandas.api.types import infer_dtype, is_categorical_dtype -from scipy.sparse.linalg import norm as sparse_norm - - +# CytoTrace wrapper def _mat_mat_corr_sparse( - X: csr_matrix, - Y: np.ndarray, + X: csr_matrix, + Y: np.ndarray, ) -> np.ndarray: """\ This function is borrow from cellrank @@ -581,7 +522,8 @@ def _mat_mat_corr_sparse( n = X.shape[1] X_bar = np.reshape(np.array(X.mean(axis=1)), (-1, 1)) - X_std = np.reshape(np.sqrt(np.array(X.power(2).mean(axis=1)) - (X_bar**2)), (-1, 1)) + X_std = np.reshape(np.sqrt(np.array(X.power(2).mean(axis=1)) - (X_bar ** 2)), + (-1, 1)) y_bar = np.reshape(np.mean(Y, axis=0), (1, -1)) y_std = np.reshape(np.std(Y, axis=0), (1, -1)) @@ -594,13 +536,13 @@ def _mat_mat_corr_sparse( def _correlation_test_helper( - X: Union[np.ndarray, spmatrix], - Y: np.ndarray, - n_perms: Optional[int] = None, - seed: Optional[int] = None, - confidence_level: float = 0.95, - **kwargs, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + X: np.ndarray | spmatrix, + Y: np.ndarray, + n_perms: int | None = None, + seed: int | None = None, + confidence_level: float = 0.95, + **kwargs, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ This function is borrow from cellrank. Compute the correlation between rows in matrix ``X`` columns of matrix ``Y``. @@ -622,13 +564,13 @@ def _correlation_test_helper( Keyword arguments for :func:`cellrank.ul._parallelize.parallelize`. Returns ------- - Correlations, p-values, corrected p-values, lower and upper bound of 95% confidence interval. - Each array if of shape ``(n_genes, n_lineages)``. + Correlations, p-values, corrected p-values, lower and upper bound of 95% + confidence interval. Each array if of shape ``(n_genes, n_lineages)``. """ def perm_test_extractor( - res: Sequence[Tuple[np.ndarray, np.ndarray]], - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + res: Sequence[tuple[np.ndarray, np.ndarray]], + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: pvals, corr_bs = zip(*res) pvals = np.sum(pvals, axis=0) / float(n_perms) @@ -641,7 +583,8 @@ def perm_test_extractor( if not (0 <= confidence_level <= 1): raise ValueError( - f"Expected `confidence_level` to be in interval `[0, 1]`, found `{confidence_level}`." + "Expected `confidence_level` to be in interval `[0, 1]`, " + + f"found `{confidence_level}`." ) n = X.shape[1] # genes x cells @@ -653,7 +596,8 @@ def perm_test_extractor( corr = _mat_mat_corr_sparse(X, Y) if issparse(X) else _mat_mat_corr_dense(X, Y) - # see: https://en.wikipedia.org/wiki/Pearson_correlation_coefficient#Using_the_Fisher_transformation + # see: + # https://en.wikipedia.org/wiki/Pearson_correlation_coefficient#Using_the_Fisher_transformation mean, se = np.arctanh(corr), 1.0 / np.sqrt(n - 3) z_score = (np.arctanh(corr) - np.arctanh(0)) * np.sqrt(n - 3) diff --git a/stlearn/spatials/trajectory/weight_optimization.py b/stlearn/spatials/trajectory/weight_optimization.py index 8bc2b363..11e09e9d 100644 --- a/stlearn/spatials/trajectory/weight_optimization.py +++ b/stlearn/spatials/trajectory/weight_optimization.py @@ -1,20 +1,21 @@ +import networkx as nx import numpy as np import pandas as pd -import networkx as nx +from tqdm import tqdm + from .global_level import global_level from .local_level import local_level from .utils import lambda_dist, resistance_distance -from tqdm import tqdm def weight_optimizing_global( - adata, - use_label=None, - list_clusters=None, - step=0.01, - k=10, - use_rep="X_pca", - n_dims=40, + adata, + use_label=None, + list_clusters=None, + step=0.01, + k=10, + use_rep="X_pca", + n_dims=40, ): # Screening PTS graph print("Screening PTS global graph...") @@ -22,12 +23,11 @@ def weight_optimizing_global( j = 0 with tqdm( - total=int(1 / step + 1), - desc="Screening", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=int(1 / step + 1), + desc="Screening", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(0, int(1 / step + 1)): - Gs.append( nx.to_scipy_sparse_array( global_level( @@ -59,9 +59,9 @@ def weight_optimizing_global( ].unique() ) with tqdm( - total=int(1 / step - 1), - desc="Calculating", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=int(1 / step - 1), + desc="Calculating", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(1, int(1 / step)): w += step @@ -100,12 +100,11 @@ def weight_optimizing_local(adata, use_label=None, cluster=None, step=0.01): Gs = [] j = 0 with tqdm( - total=int(1 / step + 1), - desc="Screening", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=int(1 / step + 1), + desc="Screening", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(0, int(1 / step + 1)): - Gs.append( local_level( adata, @@ -129,9 +128,9 @@ def weight_optimizing_local(adata, use_label=None, cluster=None, step=0.01): w = 0 with tqdm( - total=int(1 / step - 1), - desc="Calculating", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=int(1 / step - 1), + desc="Calculating", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(1, int(1 / step)): w += step diff --git a/stlearn/tl.py b/stlearn/tl.py index 073ef289..3a3c0d9e 100644 --- a/stlearn/tl.py +++ b/stlearn/tl.py @@ -1,3 +1,9 @@ from .tools import clustering -from .tools.microenv import cci from .tools.label import label +from .tools.microenv import cci + +__all__ = [ + "clustering", + "cci", + "label", +] diff --git a/stlearn/tools/clustering/__init__.py b/stlearn/tools/clustering/__init__.py index 391d4b0e..d68d6df2 100644 --- a/stlearn/tools/clustering/__init__.py +++ b/stlearn/tools/clustering/__init__.py @@ -1,3 +1,9 @@ +from .annotate import annotate_interactive from .kmeans import kmeans from .louvain import louvain -from .annotate import annotate_interactive + +__all__ = [ + "kmeans", + "louvain", + "annotate_interactive", +] diff --git a/stlearn/tools/clustering/annotate.py b/stlearn/tools/clustering/annotate.py index e195351b..d1dfabf2 100644 --- a/stlearn/tools/clustering/annotate.py +++ b/stlearn/tools/clustering/annotate.py @@ -1,8 +1,9 @@ from anndata import AnnData -from stlearn.plotting.classes_bokeh import Annotate from bokeh.io import output_notebook from bokeh.plotting import show +from stlearn.plotting.classes_bokeh import Annotate + def annotate_interactive( adata: AnnData, diff --git a/stlearn/tools/clustering/kmeans.py b/stlearn/tools/clustering/kmeans.py index 43d4d6da..c436e3c2 100644 --- a/stlearn/tools/clustering/kmeans.py +++ b/stlearn/tools/clustering/kmeans.py @@ -1,25 +1,25 @@ -from sklearn.cluster import KMeans -from anndata import AnnData -from typing import Optional, Union -import pandas as pd + import numpy as np +import pandas as pd +from anndata import AnnData from natsort import natsorted +from sklearn.cluster import KMeans def kmeans( - adata: AnnData, - n_clusters: int = 20, - use_data: str = "X_pca", - init: str = "k-means++", - n_init: int = 10, - max_iter: int = 300, - tol: float = 0.0001, - random_state: str = None, - copy_x: bool = True, - algorithm: str = "auto", - key_added: str = "kmeans", - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + n_clusters: int = 20, + use_data: str = "X_pca", + init: str = "k-means++", + n_init: int = 10, + max_iter: int = 300, + tol: float = 0.0001, + random_state: str = None, + copy_x: bool = True, + algorithm: str = "auto", + key_added: str = "kmeans", + copy: bool = False, +) -> AnnData | None: """\ Perform kmeans cluster for spatial transcriptomics data diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index 8f5ea899..87d7700d 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -1,12 +1,11 @@ +from collections.abc import Mapping, Sequence from types import MappingProxyType -from typing import Optional, Tuple, Sequence, Type, Mapping, Any, Union +from typing import Any -import numpy as np -import pandas as pd from anndata import AnnData -from natsort import natsorted from numpy.random.mtrand import RandomState from scipy.sparse import spmatrix + from stlearn._compat import Literal try: @@ -21,19 +20,19 @@ class MutableVertexPartition: def louvain( - adata: AnnData, - resolution: Optional[float] = None, - random_state: Optional[Union[int, RandomState]] = 0, - restrict_to: Optional[Tuple[str, Sequence[str]]] = None, - key_added: str = "louvain", - adjacency: Optional[spmatrix] = None, - flavor: Literal["vtraag", "igraph", "rapids"] = "vtraag", - directed: bool = True, - use_weights: bool = False, - partition_type: Optional[Type[MutableVertexPartition]] = None, - partition_kwargs: Mapping[str, Any] = MappingProxyType({}), - copy: bool = False, -) -> Optional[AnnData]: + adata: AnnData, + resolution: float | None = None, + random_state: int | RandomState | None = 0, + restrict_to: tuple[str, Sequence[str]] | None = None, + key_added: str = "louvain", + adjacency: spmatrix | None = None, + flavor: Literal["vtraag", "igraph", "rapids"] = "vtraag", # noqa: F821 + directed: bool = True, + use_weights: bool = False, + partition_type: type[MutableVertexPartition] | None = None, + partition_kwargs: Mapping[str, Any] = MappingProxyType({}), + copy: bool = False, +) -> AnnData | None: """\ Wrap function scanpy.tl.louvain Cluster cells into subgroups [Blondel08]_ [Levine15]_ [Traag17]_. diff --git a/stlearn/tools/label/label.py b/stlearn/tools/label/label.py index 2fb1960d..9dea9a20 100644 --- a/stlearn/tools/label/label.py +++ b/stlearn/tools/label/label.py @@ -3,18 +3,19 @@ """ import os + import numpy as np -import pandas as pd import scanpy as sc import stlearn.tools.microenv.cci.r_helpers as rhs def run_label_transfer( - st_data, sc_data, sc_label_col, r_path, st_label_col=None, n_highly_variable=2000 + st_data, sc_data, sc_label_col, r_path, st_label_col=None, + n_highly_variable=2000 ): """Runs Seurat label transfer.""" - st_label_col = sc_label_col if type(st_label_col) == type(None) else st_label_col + st_label_col = sc_label_col if st_label_col is None else st_label_col # Setting up the R environment # rhs.rpy2_setup(r_path) @@ -90,20 +91,20 @@ def run_label_transfer( def get_counts(data): """Gets count data from anndata if available.""" # Standard layer has counts # - if type(data.X) != np.ndarray and np.all(np.mod(data.X[0, :].todense(), 1) == 0): + if data.X is not np.ndarray and np.all(np.mod(data.X[0, :].todense(), 1) == 0): counts = data.to_df().transpose() - elif type(data.X) == np.ndarray and np.all(np.mod(data.X[0, :], 1) == 0): + elif data.X is np.ndarray and np.all(np.mod(data.X[0, :], 1) == 0): counts = data.to_df().transpose() elif ( - type(data.X) != np.ndarray - and hasattr(data, "raw") - and np.all(np.mod(data.raw.X[0, :].todense(), 1) == 0) + data.X is not np.ndarray + and hasattr(data, "raw") + and np.all(np.mod(data.raw.X[0, :].todense(), 1) == 0) ): counts = data.raw.to_adata()[data.obs_names, data.var_names].to_df().transpose() elif ( - type(data.X) == np.ndarray - and hasattr(data, "raw") - and np.all(np.mod(data.raw.X[0, :], 1) == 0) + data.X is np.ndarray + and hasattr(data, "raw") + and np.all(np.mod(data.raw.X[0, :], 1) == 0) ): counts = data.raw.to_adata()[data.obs_names, data.var_names].to_df().transpose() else: @@ -116,20 +117,20 @@ def get_counts(data): def run_rctd( - st_data, - sc_data, - sc_label_col, - r_path, - st_label_col=None, - n_highly_variable=5000, - min_cells=10, - doublet_mode="full", - n_cores=1, + st_data, + sc_data, + sc_label_col, + r_path, + st_label_col=None, + n_highly_variable=5000, + min_cells=10, + doublet_mode="full", + n_cores=1, ): """Runs RCTD for deconvolution.""" - st_label_col = sc_label_col if type(st_label_col) == type(None) else st_label_col + st_label_col = sc_label_col if st_label_col is None else st_label_col - ########### Setting up the R environment ############# + # Setting up the R environment rhs.rpy2_setup(r_path) # Adding the source R code # @@ -160,7 +161,7 @@ def run_rctd( sc_data.var["highly_variable"].values, st_data.var["highly_variable"].values ) - ###### Getting the count data (if available) ############ + # Getting the count data (if available) st_counts = get_counts(st_data) sc_counts = get_counts(sc_data) @@ -169,9 +170,9 @@ def run_rctd( st_coords = st_data.obs.loc[:, ["imagecol", "imagerow"]] sc_labels = sc_data.obs[sc_label_col].values.astype(str) - print(f"Finished extracting counts data.") + print("Finished extracting counts data.") - ####### Converting to R objects ######### + # Converting to R objects sc_labels_r = rhs.ro.StrVector(sc_labels) with rhs.localconverter(rhs.ro.default_converter + rhs.pandas2ri.converter): st_coords_r = rhs.ro.conversion.py2rpy(st_coords) @@ -179,7 +180,7 @@ def run_rctd( sc_counts_r = rhs.ro.conversion.py2rpy(sc_counts) print("Finished py->rpy conversion.") - ######## Running RCTD ########## + # Running RCTD print("Running RCTD...") rctd_proportions_r = rctd_r( st_counts_r, @@ -201,27 +202,27 @@ def run_rctd( st_data_orig.obs[st_label_col] = labels st_data_orig.obs[st_label_col] = st_data_orig.obs[st_label_col].astype("category") st_data_orig.uns[st_label_col] = rctd_proportions.loc[ - st_data_orig.obs_names.values, : - ] + st_data_orig.obs_names.values, : + ] print(f"Spot labels added to st_data.obs[{st_label_col}].") print(f"Spot label scores added to st_data.uns[{st_label_col}].") def run_singleR( - st_data, - sc_data, - sc_label_col, - r_path, - st_label_col=None, - n_highly_variable=5000, - n_centers=3, - de_n=200, - de_method="t", + st_data, + sc_data, + sc_label_col, + r_path, + st_label_col=None, + n_highly_variable=5000, + n_centers=3, + de_n=200, + de_method="t", ): """Runs SingleR spot annotation.""" - st_label_col = sc_label_col if type(st_label_col) == type(None) else st_label_col - ########### Setting up the R environment ############# + st_label_col = sc_label_col if st_label_col is None else st_label_col + # Setting up the R environment rhs.rpy2_setup(r_path) # Adding the source R code # @@ -253,13 +254,13 @@ def run_singleR( ) sc_data = sc_data[:, genes_bool] st_data = st_data[:, genes_bool] - print(f"Finished selecting & subsetting to hvgs.") + print("Finished selecting & subsetting to hvgs.") # Extracting the relevant information from anndatas # st_expr_df = st_data.to_df().transpose() sc_expr_df = sc_data.to_df().transpose() sc_labels = sc_data.obs[sc_label_col].values.astype(str) - print(f"Finished extracting data.") + print("Finished extracting data.") # R conversion of the data # sc_labels_r = rhs.ro.StrVector(sc_labels) diff --git a/stlearn/tools/microenv/cci/__init__.py b/stlearn/tools/microenv/cci/__init__.py index 343fa4f3..efea98c1 100644 --- a/stlearn/tools/microenv/cci/__init__.py +++ b/stlearn/tools/microenv/cci/__init__.py @@ -4,4 +4,13 @@ # from .het import edge_core, get_between_spot_edge_array # from .merge import merge # from .permutation import get_rand_pairs -from .analysis import load_lrs, grid, run, adj_pvals, run_lr_go, run_cci +from .analysis import adj_pvals, grid, load_lrs, run, run_cci, run_lr_go + +__all__ = [ + "load_lrs", + "grid", + "run", + "adj_pvals", + "run_lr_go", + "run_cci", +] diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index d6421a64..81ec454d 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -3,45 +3,48 @@ """ import os + import numba -from numba import types -from numba.typed import List import numpy as np import pandas as pd -from typing import Union from anndata import AnnData +from statsmodels.stats.multitest import multipletests from tqdm import tqdm -from .base import calc_neighbours, get_lrs_scores, calc_distance -from .permutation import perform_spot_testing + +from .base import calc_distance, calc_neighbours, get_lrs_scores from .go import run_GO from .het import ( count, - get_neighbourhoods, get_data_for_counting, get_interaction_matrix, get_interaction_pvals, + get_neighbourhoods, grid_parallel, ) -from statsmodels.stats.multitest import multipletests +from .permutation import perform_spot_testing -################################################################################ -# Functions related to Ligand-Receptor interactions # -################################################################################ -def load_lrs(names: Union[str, list, None] = None, species: str = "human") -> np.array: - """Loads inputted LR database, & concatenates into consistent database set of pairs without duplicates. If None loads 'connectomeDB2020_lit'. +# Functions related to Ligand-Receptor interactions +def load_lrs(names: str | list | None = None, species: str = "human") -> np.array: + """Loads inputted LR database, & concatenates into consistent database set of + pairs without duplicates. If None loads 'connectomeDB2020_lit'. Parameters ---------- - names: list Databases to load, options: 'connectomeDB2020_lit' (literature verified), 'connectomeDB2020_put' (putative). If more than one specified, loads all & removes duplicates. - species: str Format of the LR genes, either 'human' or 'mouse'. + names: list + Databases to load, options: 'connectomeDB2020_lit' (literature verified), + 'connectomeDB2020_put' (putative). If more than one specified, loads all & + removes duplicates. + species: str + Format of the LR genes, either 'human' or 'mouse'. Returns ------- - lrs: np.array lr pairs from the database in format ['L1_R1', 'LN_RN'] + lrs: np.array + lr pairs from the database in format ['L1_R1', 'LN_RN'] """ - if type(names) == type(None): + if names is None: names = ["connectomeDB2020_lit"] - if type(names) == str: + if names is str: names = [names] path = os.path.dirname(os.path.realpath(__file__)) @@ -103,7 +106,7 @@ def grid( print("Gridding...") # Setting threads for paralellisation # - if type(n_cpus) != type(None): + if n_cpus is not None: numba.set_num_threads(n_cpus) # Retrieving the coordinates of each grid # @@ -122,10 +125,10 @@ def grid( grid_expr = np.zeros((n_squares, adata.shape[1])) grid_coords = np.zeros((n_squares, 2)) - grid_cell_counts = np.zeros((n_squares), dtype=np.int64) - # If use_label specified, then will generate deconvolution information + grid_cell_counts = np.zeros(n_squares, dtype=np.int64) + # If use_label is specified, then it will generate deconvolution information cell_labels, cell_set, cell_info = None, None, None - if type(use_label) != type(None): + if use_label is not None: cell_labels = adata.obs[use_label].values.astype(str) cell_set = np.unique(cell_labels).astype(str) cell_info = np.zeros((n_squares, len(cell_set)), dtype=np.float64) @@ -143,7 +146,7 @@ def grid( grid_cell_counts, grid_expr, adata.X, - type(use_label) != type(None), + use_label is not None, cell_labels, cell_info, cell_set, @@ -162,7 +165,7 @@ def grid( grid_data.obsm["spatial"] = grid_coords grid_data.uns["spatial"] = adata.uns["spatial"] - if type(use_label) != type(None): + if use_label is not None: grid_data.uns[use_label] = pd.DataFrame( cell_info, index=grid_data.obs_names.values.astype(str), columns=cell_set ) @@ -177,7 +180,7 @@ def grid( # Subsetting to only gridded spots that contain cells # grid_data = grid_data[grid_data.obs["n_cells"] > 0, :].copy() - if type(use_label) != type(None): + if use_label is not None: grid_data.uns[use_label] = grid_data.uns[use_label].loc[grid_data.obs_names, :] grid_data.uns["grid_counts"] = grid_counts @@ -250,7 +253,8 @@ def run( adata.uns['lr_summary'] Summary of significant spots detected per LR, the LRs listed in the index is the same order of LRs in the columns of - results stored in adata.obsm below. Hence the order of this must be maintained. + results stored in adata.obsm below. Hence, the order of this must be + maintained. adata.obsm Additional keys are added; 'lr_scores', 'lr_sig_scores', 'p_vals', 'p_adjs', '-log10(p_adjs)'. All are numpy matrices, with columns @@ -258,10 +262,11 @@ def run( is the raw scores, while 'lr_sig_scores' is the same except only for significant scores; non-significant scores are set to zero. adata.obsm['het'] - Only if use_label specified; contains the counts of the cell types found per spot. + Only if use_label specified; contains the counts of the cell types found + per spot. """ - # Setting threads for paralellisation # - if type(n_cpus) != type(None): + # Setting threads for parallelisation + if n_cpus is not None: numba.set_num_threads(n_cpus) # Making sure none of the var_names contains '_' already, these will need @@ -306,10 +311,10 @@ def run( ) # Conduct with cell heterogeneity info if label_transfer provided # - cell_het = type(use_label) != type(None) and use_label in adata.uns.keys() + cell_het = use_label is not None and use_label in adata.uns.keys() if cell_het: if verbose: - print("Calculating cell hetereogeneity...") + print("Calculating cell heterogeneity...") # Calculating cell heterogeneity # count(adata, distance=distance, use_label=use_label, use_het=use_label) @@ -402,12 +407,12 @@ def adj_pvals( spot_padjs = multipletests(lr_ps, method=adj_method)[1] padjs[spot_indices, lr_i] = spot_padjs sig_scores[spot_indices[spot_padjs >= pval_adj_cutoff], lr_i] = 0 - elif type(correct_axis) == type(None): + elif correct_axis is None: padjs = ps.copy() sig_scores[padjs >= pval_adj_cutoff] = 0 else: raise Exception( - f"Invalid correct_axis input, must be one of: " f"'LR', 'spot', or None" + "Invalid correct_axis input, must be one of: 'LR', 'spot', or None" ) # Counting spots significant per lr # @@ -419,7 +424,7 @@ def adj_pvals( adata.uns["lr_summary"].loc[:, "n_spots_sig_pval"] = lr_counts_pval new_order = np.argsort(-adata.uns["lr_summary"].loc[:, "n_spots_sig"].values) adata.uns["lr_summary"] = adata.uns["lr_summary"].iloc[new_order, :] - print(f"Updated adata.uns[lr_summary]") + print("Updated adata.uns[lr_summary]") scores_ordered = scores[:, new_order] sig_scores_ordered = sig_scores[:, new_order] ps_ordered = ps[:, new_order] @@ -469,18 +474,19 @@ def run_lr_go( q_cutoff: float Q-value cutoff below which results will be returned. onts: str - As per clusterProfiler; One of "BP", "MF", and "CC" subontologies, or "ALL" for all three. + As per clusterProfiler; One of "BP", "MF", and "CC" subontologies, or "ALL" + for all three. Returns ------- adata: AnnData Relevant information stored in adata.uns['lr_go'] """ - #### Making sure inputted correct species #### + # Making sure inputted correct species all_species = ["human", "mouse"] if species not in all_species: raise Exception(f"Got {species} for species, must be one of " f"{all_species}") - #### Getting the genes from the top LR pairs #### + # Getting the genes from the top LR pairs if "lr_summary" not in adata.uns: raise Exception("Need to run st.tl.cci.run first.") lrs = adata.uns["lr_summary"].index.values.astype(str) @@ -488,12 +494,12 @@ def run_lr_go( top_lrs = lrs[n_sig > min_sig_spots][0:n_top] top_genes = np.unique([lr.split("_") for lr in top_lrs]) - ## Determining the background genes if not inputted ## - if type(bg_genes) == type(None): + # Determining the background genes if not inputted + if bg_genes is None: all_lrs = load_lrs("connectomeDB2020_put") bg_genes = np.unique([lr_.split("_") for lr_ in all_lrs]) - #### Running the GO analysis #### + # Running the GO analysis go_results = run_GO( top_genes, bg_genes, @@ -508,9 +514,7 @@ def run_lr_go( print("GO results saved to adata.uns['lr_go']") -################################################################################ -# Functions for calling Celltype-Celltype interactions # -################################################################################ +# Functions for calling Celltype-Celltype interactions def run_cci( adata: AnnData, use_label: str, @@ -523,7 +527,8 @@ def run_cci( n_cpus: int = 1, verbose: bool = True, ): - """Calls significant celltype-celltype interactions based on cell-type data randomisation. + """Calls significant celltype-celltype interactions based on cell-type data + randomisation. Parameters ---------- @@ -591,7 +596,7 @@ def run_cci( subsetted to significant CCIs. """ # Setting threads for paralellisation # - if type(n_cpus) != type(None): + if n_cpus is not None: numba.set_num_threads(n_cpus) ran_lr = "lr_summary" in adata.uns @@ -639,12 +644,12 @@ def run_cci( msg = msg + "Rows do not correspond to adata.obs_names.\n" raise Exception(msg) - #### Checking for case where have cell types that are never dominant - #### in a spot, so need to include these in all_set + # Checking for case where have cell types that are never dominant + # in a spot, so need to include these in all_set if len(all_set) < adata.uns[uns_key].shape[1]: all_set = adata.uns[uns_key].columns.values.astype(str) - #### Getting minimum necessary information for edge counting #### + # Getting minimum necessary information for edge counting if verbose: print("Getting cached neighbourhood information...") # Getting the neighbourhoods # @@ -676,20 +681,21 @@ def run_cci( per_lr_cci = {} # Per LR significant CCI counts # per_lr_cci_pvals = {} # Per LR CCI p-values # per_lr_cci_raw = {} # Per LR raw CCI counts # - lr_n_spot_cci = np.zeros((lr_summary.shape[0])) - lr_n_spot_cci_sig = np.zeros((lr_summary.shape[0])) - lr_n_cci_sig = np.zeros((lr_summary.shape[0])) + lr_n_spot_cci = np.zeros(lr_summary.shape[0]) + lr_n_spot_cci_sig = np.zeros(lr_summary.shape[0]) + lr_n_cci_sig = np.zeros(lr_summary.shape[0]) with tqdm( total=len(best_lrs), - desc=f"Counting celltype-celltype interactions per LR and permutating {n_perms} times.", + desc="Counting celltype-celltype interactions per LR and permuting " + + f"{n_perms} times.", bar_format="{l_bar}{bar} [ time left: {remaining} ]", - disable=verbose == False, + disable=verbose is False, ) as pbar: for i, best_lr in enumerate(best_lrs): - l, r = best_lr.split("_") + ligand, receptor = best_lr.split("_") - L_bool = lr_expr.loc[:, l].values > 0 - R_bool = lr_expr.loc[:, r].values > 0 + L_bool = lr_expr.loc[:, ligand].values > 0 + R_bool = lr_expr.loc[:, receptor].values > 0 lr_index = np.where(adata.uns["lr_summary"].index.values == best_lr)[0][0] sig_bool = adata.obsm[col][:, lr_index] > 0 diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tools/microenv/cci/base.py index ecea7ecc..6f88750e 100644 --- a/stlearn/tools/microenv/cci/base.py +++ b/stlearn/tools/microenv/cci/base.py @@ -1,36 +1,47 @@ import numpy as np import pandas as pd import scipy as sc -from numba import njit, prange -from numba.typed import List import scipy.spatial as spatial from anndata import AnnData +from numba import njit, prange +from numba.typed import List + from .het import create_grids def lr( - adata: AnnData, - use_lr: str = "cci_lr", - distance: float = None, - verbose: bool = True, - neighbours: list = None, - fast: bool = True, + adata: AnnData, + use_lr: str = "cci_lr", + distance: float = None, + verbose: bool = True, + neighbours: list = None, + fast: bool = True, ) -> AnnData: - """Calculate the proportion of known ligand-receptor co-expression among the neighbouring spots or within spots + """Calculate the proportion of known ligand-receptor co-expression among the + neighbouring spots or within spots Parameters ---------- - adata: AnnData The data object to scan - use_lr: str object to keep the result (default: adata.uns['cci_lr']) - distance: float Distance to determine the neighbours (default: closest), distance=0 means within spot - neighbours: list List of the neighbours for each spot, if None then computed. Useful for speeding up function. - fast: bool Whether to use the fast implimentation or not. + adata: AnnData + The data object to scan + use_lr: str + object to keep the result (default: adata.uns['cci_lr']) + distance: float + Distance to determine the neighbours (default: closest), distance=0 means + within spot + neighbours: list + List of the neighbours for each spot, if None then computed. Useful for + speeding up function. + fast: bool + Whether to use the fast implementation or not. Returns ------- - adata: AnnData The data object including the results + adata: AnnData + The data object including the results """ - # automatically calculate distance if not given, won't overwrite distance=0 which is within-spot + # automatically calculate distance if not given, won't overwrite distance=0 + # which is within-spot distance = calc_distance(adata, distance) # # expand the LR pairs list by swapping ligand-receptor positions @@ -41,7 +52,7 @@ def lr( print("Altogether " + str(spot_lr1.shape[1]) + " valid L-R pairs") # get neighbour spots for each spot according to the specified distance - if type(neighbours) == type(None): + if neighbours is None: neighbours = calc_neighbours(adata, distance, index=fast) # Calculating the scores, can have either the fast or the pandas version # @@ -65,51 +76,64 @@ def calc_distance(adata: AnnData, distance: float): distance=0 which is within-spot. Parameters ---------- - adata: AnnData The data object to scan - distance: float Distance to determine the neighbours (default: closest), distance=0 means within spot + adata: AnnData + The data object to scan + distance: float + Distance to determine the neighbours (default: closest), distance=0 means + within spot Returns ------- - distance: float The automatically calcualted distance (or inputted distance) + distance: float + The automatically calcualted distance (or inputted distance) """ if not distance and distance != 0: # for arranged-spots scalefactors = next(iter(adata.uns["spatial"].values()))["scalefactors"] library_id = list(adata.uns["spatial"].keys())[0] distance = ( - scalefactors["spot_diameter_fullres"] - * scalefactors[ - "tissue_" + adata.uns["spatial"][library_id]["use_quality"] + "_scalef" - ] - * 2 + scalefactors["spot_diameter_fullres"] + * scalefactors[ + "tissue_" + adata.uns["spatial"][library_id][ + "use_quality"] + "_scalef" + ] + * 2 ) return distance def get_lrs_scores( - adata: AnnData, - lrs: np.array, - neighbours: np.array, - het_vals: np.array, - min_expr: float, - filter_pairs: bool = True, - spot_indices: np.array = None, + adata: AnnData, + lrs: np.array, + neighbours: np.array, + het_vals: np.array, + min_expr: float, + filter_pairs: bool = True, + spot_indices: np.array = None, ): """Gets the scores for the indicated set of LR pairs & the heterogeneity values. Parameters ---------- - adata: AnnData See run() doc-string. - lrs: np.array See run() doc-string. - neighbours: np.array Array of arrays with indices specifying neighbours of each spot. - het_vals: np.array Cell heterogeneity counts per spot. - min_expr: float Minimum gene expression of either L or R for spot to be considered to have reasonable score. - filter_pairs: bool Whether to filter to valid pairs or not. - spot_indices: np.array Array of integers speci + adata: AnnData + See run() doc-string. + lrs: np.array + See run() doc-string. + neighbours: np.array + Array of arrays with indices specifying neighbours of each spot. + het_vals: np.array + Cell heterogeneity counts per spot. + min_expr: float + Minimum gene expression of either L or R for spot to be considered to + have reasonable score. + filter_pairs: bool + Whether to filter to valid pairs or not. + spot_indices: np.array + Array of integers speci Returns ------- lrs: np.array lr pairs from the database in format ['L1_R1', 'LN_RN'] """ - if type(spot_indices) == type(None): + if spot_indices is None: spot_indices = np.array(list(range(len(adata))), dtype=np.int32) spot_lr1s = get_spot_lrs( @@ -121,7 +145,7 @@ def get_lrs_scores( if filter_pairs: lrs = np.array( [ - "_".join(spot_lr1s.columns.values[i : i + 2]) + "_".join(spot_lr1s.columns.values[i: i + 2]) for i in range(0, spot_lr1s.shape[1], 2) ] ) @@ -138,22 +162,28 @@ def get_lrs_scores( def get_spot_lrs( - adata: AnnData, - lr_pairs: list, - lr_order: bool, - filter_pairs: bool = True, + adata: AnnData, + lr_pairs: list, + lr_order: bool, + filter_pairs: bool = True, ): """ Parameters ---------- - adata: AnnData The adata object to scan - lr_pairs: list List of the lr pairs (e.g. ['L1_R1', 'L2_R2',...] - lr_order: bool Forward version of the spot lr pairs (L1_R1), False indicates reverse (R1_L1) - filter_pairs: bool Whether to filter the pairs or not (check if present before subsetting). + adata (AnnData): + The adata object to scan + lr_pairs (list): + List of the lr pairs (e.g. ['L1_R1', 'L2_R2',...] + lr_order (bool): + Forward version of the spot lr pairs (L1_R1), False indicates reverse (R1_L1) + filter_pairs (bool): + Whether to filter the pairs or not (check if present before sub-setting). Returns ------- - spot_lrs: pd.DataFrame Spots*GeneOrder, in format l1, r1, ... ln, rn if lr_order True, else r1, l1, ... rn, ln + spot_lrs: pd.DataFrame + Spots*GeneOrder, in format l1, r1, ... ln, rn if lr_order True, else r1, + l1, ... rn, ln """ df = adata.to_df() pairs_rev = [f'{pair.split("_")[1]}_{pair.split("_")[0]}' for pair in lr_pairs] @@ -168,27 +198,36 @@ def get_spot_lrs( if lr.split("_")[0] in df.columns and lr.split("_")[1] in df.columns ] - lr_cols = [pair.split("_")[int(lr_order == False)] for pair in pairs_wRev] + lr_cols = [pair.split("_")[int(lr_order is False)] for pair in pairs_wRev] spot_lrs = df[lr_cols] return spot_lrs def calc_neighbours( - adata: AnnData, - distance: float = None, - index: bool = True, - verbose: bool = True, + adata: AnnData, + distance: float = None, + index: bool = True, + verbose: bool = True, ) -> List: - """Calculate the proportion of known ligand-receptor co-expression among the neighbouring spots or within spots + """Calculate the proportion of known ligand-receptor co-expression among the + neighbouring spots or within spots Parameters ---------- - adata: AnnData The data object to scan - distance: float Distance to determine the neighbours (default: closest), distance=0 means within spot - index: bool Indicates whether to return neighbours as indices to other spots or names of other spots. + adata (AnnData): + The data object to scan + distance (float): + Distance to determine the neighbours (default: closest), distance=0 means + within spot + index (bool): + Indicates whether to return neighbours as indices to other spots or names of + other spots. + verbose (bool): + Display debugging information Returns ------- - neighbours: numba.typed.List List of np.array's indicating neighbours by indices for each spot. + neighbours (numba.typed.List): + List of np.array's indicating neighbours by indices for each spot. """ if verbose: print("Calculating neighbours...") @@ -219,7 +258,7 @@ def calc_neighbours( n_neighs = np.array([len(neigh) for neigh in neighbours]) if verbose: print( - f"{len(np.where(n_neighs==0)[0])} spots with no neighbours, " + f"{len(np.where(n_neighs == 0)[0])} spots with no neighbours, " f"{int(np.median(n_neighs))} median spot neighbours." ) @@ -234,22 +273,27 @@ def calc_neighbours( @njit def lr_core( - spot_lr1: np.ndarray, - spot_lr2: np.ndarray, - neighbours: List, - min_expr: float, - spot_indices: np.array, + spot_lr1: np.ndarray, + spot_lr2: np.ndarray, + neighbours: List, + min_expr: float, + spot_indices: np.array, ) -> np.ndarray: """Calculate the lr scores for each spot. Parameters ---------- - spot_lr1: np.ndarray Spots*Ligands - spot_lr2: np.ndarray Spots*Receptors - neighbours: numba.typed.List List of np.array's indicating neighbours by indices for each spot. - min_expr: float Minimum expression for gene to be considered expressed. + spot_lr1: np.ndarray + Spots*Ligands + spot_lr2: np.ndarray + Spots*Receptors + neighbours: numba.typed.List + List of np.array's indicating neighbours by indices for each spot. + min_expr: float + Minimum expression for gene to be considered expressed. Returns ------- - lr_scores: numpy.ndarray Cells*LR-scores. + lr_scores: numpy.ndarray + Cells*LR-scores. """ # Calculating mean of lr2 expressions from neighbours of each spot nb_lr2 = np.zeros((len(spot_indices), spot_lr2.shape[1]), np.float64) @@ -263,30 +307,35 @@ def lr_core( nb_lr2[i, :] = nb_expr_mean scores = ( - spot_lr1[spot_indices, :] * (nb_lr2 > min_expr) - + (spot_lr1[spot_indices, :] > min_expr) * nb_lr2 + spot_lr1[spot_indices, :] * (nb_lr2 > min_expr) + + (spot_lr1[spot_indices, :] > min_expr) * nb_lr2 ) spot_lr = scores.sum(axis=1) return spot_lr / 2 def lr_pandas( - spot_lr1: np.ndarray, - spot_lr2: np.ndarray, - neighbours: list, + spot_lr1: np.ndarray, + spot_lr2: np.ndarray, + neighbours: list, ) -> np.ndarray: """Calculate the lr scores for each spot. Parameters ---------- - spot_lr1: pd.DataFrame Cells*Ligands - spot_lr2: pd.DataFrame Cells*Receptors - neighbours: list List of neighbours by indices for each spot. + spot_lr1 (pd.DataFrame): + Cells*Ligands + spot_lr2 (pd.DataFrame): + Cells*Receptors + neighbours (list): + List of neighbours by indices for each spot. Returns ------- - lr_scores: numpy.ndarray Cells*LR-scores. + lr_scores (numpy.ndarray): + Cells*LR-scores. """ - # function to calculate mean of lr2 expression between neighbours or within spot (distance==0) for each spot + # function to calculate mean of lr2 expression between neighbours or within + # spot (distance==0) for each spot def mean_lr2(x): # get lr2 expressions from the neighbour(s) n_spots = neighbours[spot_lr2.index.tolist().index(x.name)] @@ -314,29 +363,35 @@ def mean_lr2(x): @njit(parallel=True) def get_scores( - spot_lr1s: np.ndarray, - spot_lr2s: np.ndarray, - neighbours: List, - het_vals: np.array, - min_expr: float, - spot_indices: np.array, + spot_lr1s: np.ndarray, + spot_lr2s: np.ndarray, + neighbours: List, + het_vals: np.array, + min_expr: float, + spot_indices: np.array, ) -> np.array: """Calculates the scores. Parameters ---------- - spot_lr1s: np.ndarray Spots*GeneOrder1, in format l1, r1, ... ln, rn - spot_lr2s: np.ndarray Spots*GeneOrder2, in format r1, l1, ... rn, ln - het_vals: np.ndarray Spots*Het counts - neighbours: numba.typed.List List of np.array's indicating neighbours by indices for each spot. - min_expr: float Minimum expression for gene to be considered expressed. + spot_lr1s: np.ndarray + Spots*GeneOrder1, in format l1, r1, ... ln, rn + spot_lr2s: np.ndarray + Spots*GeneOrder2, in format r1, l1, ... rn, ln + het_vals: np.ndarray + Spots*Het counts + neighbours: numba.typed.List + List of np.array's indicating neighbours by indices for each spot. + min_expr: float + Minimum expression for gene to be considered expressed. Returns ------- - spot_scores: np.ndarray Spots*LR pair of the LR scores per spot. + spot_scores: np.ndarray + Spots*LR pair of the LR scores per spot. """ spot_scores = np.zeros((len(spot_indices), spot_lr1s.shape[1] // 2), np.float64) for i in prange(0, spot_lr1s.shape[1] // 2): i_ = i * 2 # equivalent to range(0, spot_lr1s.shape[1], 2) - spot_lr1, spot_lr2 = spot_lr1s[:, i_ : (i_ + 2)], spot_lr2s[:, i_ : (i_ + 2)] + spot_lr1, spot_lr2 = spot_lr1s[:, i_: (i_ + 2)], spot_lr2s[:, i_: (i_ + 2)] lr_scores = lr_core(spot_lr1, spot_lr2, neighbours, min_expr, spot_indices) # The merge scores # lr_scores = np.multiply(het_vals[spot_indices], lr_scores) @@ -345,25 +400,33 @@ def get_scores( def lr_grid( - adata: AnnData, - num_row: int = 10, - num_col: int = 10, - use_lr: str = "cci_lr_grid", - radius: int = 1, - verbose: bool = True, + adata: AnnData, + num_row: int = 10, + num_col: int = 10, + use_lr: str = "cci_lr_grid", + radius: int = 1, + verbose: bool = True, ) -> AnnData: - """Calculate the proportion of known ligand-receptor co-expression among the neighbouring grids or within each grid + """Calculate the proportion of known ligand-receptor co-expression among the + neighbouring grids or within each grid Parameters ---------- - adata: AnnData The data object to scan - num_row: int Number of grids on height - num_col: int Number of grids on width - use_lr: str object to keep the result (default: adata.uns['cci_lr']) - radius: int Distance to determine the neighbour grids (default: 1=nearest), radius=0 means within grid + adata: AnnData + The data object to scan + num_row: int + Number of grids on height + num_col: int + Number of grids on width + use_lr: str + object to keep the result (default: adata.uns['cci_lr']) + radius: int + Distance to determine the neighbour grids (default: 1=nearest), + radius=0 means within grid Returns ------- - adata: AnnData The data object with the cci_lr grid result updated + adata: AnnData + The data object with the cci_lr grid result updated """ # prepare data as pd.dataframe @@ -388,7 +451,7 @@ def lr_grid( & (coor["imagecol"] < grid[0] + width) & (coor["imagerow"] < grid[1]) & (coor["imagerow"] > grid[1] - height) - ] + ] df_grid.loc[n] = df.loc[spots.index].sum() # expand the LR pairs list by swapping ligand-receptor positions @@ -406,7 +469,8 @@ def lr_grid( if verbose: print("Altogether " + str(len(avail)) + " valid L-R pairs") - # function to calculate mean of lr2 expression between neighbours or within spot (distance==0) for each spot + # function to calculate mean of lr2 expression between neighbours or within spot + # (distance==0) for each spot def mean_lr2(x): # get the neighbour(s)' lr2 expressions nbs = grid_lr2.loc[neighbours[df_grid.index.tolist().index(x.name)], :] diff --git a/stlearn/tools/microenv/cci/base_grouping.py b/stlearn/tools/microenv/cci/base_grouping.py index 882bb011..b8a05e7a 100644 --- a/stlearn/tools/microenv/cci/base_grouping.py +++ b/stlearn/tools/microenv/cci/base_grouping.py @@ -2,38 +2,49 @@ similar tissues. """ -from stlearn.pl import het_plot -from sklearn.cluster import DBSCAN, AgglomerativeClustering -from anndata import AnnData -from tqdm import tqdm +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt import seaborn as sb +from anndata import AnnData +from sklearn.cluster import DBSCAN, AgglomerativeClustering +from tqdm import tqdm + +from stlearn.pl import het_plot def get_hotspots( - adata: AnnData, - lr_scores: np.ndarray, - lrs: np.array, - eps: float, - quantile=0.05, - verbose=True, - plot_diagnostics: bool = False, - show_plot: bool = False, + adata: AnnData, + lr_scores: np.ndarray, + lrs: np.array, + eps: float, + quantile=0.05, + verbose=True, + plot_diagnostics: bool = False, + show_plot: bool = False, ): - """Determines the hotspots for the inputted scores by progressively setting more stringent cutoffs & cluster in space, chooses point which maximises number of clusters. + """Determines the hotspots for the inputted scores by progressively setting + more stringent cutoffs & cluster in space, chooses point which maximises number + of clusters. Parameters ---------- - adata: AnnData The data object - lr_scores: np.ndarray LR_pair*Spots containing the LR scores. - lrs: np.array The LR_pairs, in-line with the rows of scores. - eps: float The eps parameter used in DBScan to get the number of clusters. - quantile: float The quantiles to use for the cutoffs, if 0.05 then will take non-zero quantiles of 0.05, 0.1,..., 1 quantiles to cluster. + adata: AnnData + The data object + lr_scores: np.ndarray + LR_pair*Spots containing the LR scores. + lrs: np.array + The LR_pairs, in-line with the rows of scores. + eps: float + The eps parameter used in DBScan to get the number of clusters. + quantile: float + The quantiles to use for the cutoffs, if 0.05 then will take non-zero + quantiles of 0.05, 0.1,..., 1 quantiles to cluster. Returns ------- - lr_hot_scores: np.ndarray, lr_cutoffs: np.array First is the LR scores for just the hotspots, second is the cutoff used to get those LR_scores. + lr_hot_scores: np.ndarray, lr_cutoffs: np.array + First is the LR scores for just the hotspots, second is the cutoff used to + get those LR_scores. """ coors = adata.obs[["imagerow", "imagecol"]].values lr_summary, lr_hot_scores = hotspot_core( @@ -107,26 +118,25 @@ def get_hotspots( adata.obsm["cluster_scores"] = cluster_scores if verbose: - print(f"\tSummary values of lrs in adata.uns['lr_summary'].") + print("\tSummary values of lrs in adata.uns['lr_summary'].") print( - f"\tMatrix of lr scores in same order as the summary in adata.obsm['lr_scores']." - ) - print(f"\tMatrix of the hotspot scores in adata.obsm['lr_hot_scores'].") - print( - f"\tMatrix of the mean LR cluster scores in adata.obsm['cluster_scores']." + "\tMatrix of lr scores in same order as the summary in " + + "adata.obsm['lr_scores']." ) + print("\tMatrix of the hotspot scores in adata.obsm['lr_hot_scores'].") + print("\tMatrix of the mean LR cluster scores in adata.obsm['cluster_scores'].") def hotspot_core( - lr_scores, - lrs, - coors, - eps, - quantile, - plot_diagnostics=False, - adata=None, - verbose=True, - max_score=False, + lr_scores, + lrs, + coors, + eps, + quantile, + plot_diagnostics=False, + adata=None, + verbose=True, + max_score=False, ): """Made code for getting the hotspot information.""" score_copy = lr_scores.copy() @@ -137,7 +147,7 @@ def hotspot_core( # cols: spot_counts, cutoff, hotspot_counts, lr_cluster lr_summary = np.zeros((score_copy.shape[0], 4)) - ### Also creating grouping lr_pairs by quantiles to plot diagnostics ### + # Also creating grouping lr_pairs by quantiles to plot diagnostics if plot_diagnostics: lr_quantiles = [(i / 6) for i in range(1, 7)][::-1] lr_mean_scores = np.apply_along_axis(non_zero_mean, 1, score_copy) @@ -149,10 +159,10 @@ def hotspot_core( # Determining the cutoffs for hotspots # with tqdm( - total=len(lrs), - desc="Removing background lr scores...", - bar_format="{l_bar}{bar}", - disable=verbose == False, + total=len(lrs), + desc="Removing background lr scores...", + bar_format="{l_bar}{bar}", + disable=verbose is False, ) as pbar: for i, lr_ in enumerate(lrs): lr_score_ = score_copy[i, :] @@ -185,7 +195,7 @@ def hotspot_core( lr_summary[i, 2] = len(np.where(lr_score_ > 0)[0]) # Adding the diagnostic plots # - if plot_diagnostics and lr_ in quant_lrs and type(adata) != type(None): + if plot_diagnostics and lr_ in quant_lrs and adata is not None: add_diagnostic_plots( adata, i, @@ -211,17 +221,17 @@ def non_zero_mean(vals): def add_diagnostic_plots( - adata, - i, - lr_, - quant_lrs, - lr_quantiles, - lr_scores, - lr_hot_scores, - axes, - cutoffs, - n_clusters, - best_cutoff, + adata, + i, + lr_, + quant_lrs, + lr_quantiles, + lr_scores, + lr_hot_scores, + axes, + cutoffs, + n_clusters, + best_cutoff, ): """Adds diagnostic plots for the quantile LR pair to a figure to illustrate \ how the cutoff is functioning. @@ -230,7 +240,7 @@ def add_diagnostic_plots( # Scatter plot # axes[q_i][0].scatter(cutoffs, n_clusters) - axes[q_i][0].set_title(f"n_clusts*mean_spot_score vs cutoff") + axes[q_i][0].set_title("n_clusts*mean_spot_score vs cutoff") axes[q_i][0].set_xlabel("cutoffs") axes[q_i][0].set_ylabel("n_clusts*mean_spot_score") diff --git a/stlearn/tools/microenv/cci/go.py b/stlearn/tools/microenv/cci/go.py index eff77d09..ee7b98fe 100644 --- a/stlearn/tools/microenv/cci/go.py +++ b/stlearn/tools/microenv/cci/go.py @@ -1,6 +1,7 @@ """Wrapper for performing the LR GO analysis.""" import os + import stlearn.tools.microenv.cci.r_helpers as rhs @@ -20,7 +21,7 @@ def run_GO(genes, bg_genes, species, r_path, p_cutoff=0.01, q_cutoff=0.5, onts=" # Running the function on the genes # genes_r = rhs.ro.StrVector(genes) - if type(bg_genes) != type(None): + if bg_genes is not None: bg_genes_r = rhs.ro.StrVector(bg_genes) else: bg_genes_r = rhs.ro.r["as.null"]() diff --git a/stlearn/tools/microenv/cci/het.py b/stlearn/tools/microenv/cci/het.py index bc6fb221..7b3a7bdf 100644 --- a/stlearn/tools/microenv/cci/het.py +++ b/stlearn/tools/microenv/cci/het.py @@ -1,16 +1,15 @@ import numpy as np import pandas as pd -from anndata import AnnData import scipy.spatial as spatial - +from anndata import AnnData +from numba import jit, njit, prange from numba.typed import List -from numba import njit, jit, prange from stlearn.tools.microenv.cci.het_helpers import ( + add_unique_edges, edge_core, get_between_spot_edge_array, get_data_for_counting, - add_unique_edges, get_neighbourhoods, init_edge_list, ) @@ -26,20 +25,28 @@ def count( """Count the cell type densities Parameters ---------- - adata: AnnData The data object including the cell types to count - use_label: The cell type results to use in counting - use_het: The stoarge place for result - distance: int Distance to determine the neighbours (default is the nearest neighbour), distance=0 means within spot + adata: AnnData + The data object including the cell types to count + use_label: + The cell type results to use in counting + use_het: + The storage place for result + distance: int + Distance to determine the neighbours (default is the nearest neighbour), + distance=0 means within spot Returns ------- - adata: AnnData With the counts of specified clusters in nearby spots stored as adata.uns['het'] + adata: AnnData + With the counts of specified clusters in nearby spots stored as + adata.uns['het'] """ library_id = list(adata.uns["spatial"].keys())[0] # between spot if distance != 0: - # automatically calculate distance if not given, won't overwrite distance=0 which is within-spot + # automatically calculate distance if not given, won't overwrite distance=0 + # which is within-spot if not distance: # calculate default neighbour distance scalefactors = next(iter(adata.uns["spatial"].values()))["scalefactors"] @@ -92,10 +99,15 @@ def get_edges(adata: AnnData, L_bool: np.array, R_bool: np.array, sig_bool: np.a Parameters ---------- - adata: AnnData - L_bool: np.array len(L_bool)==len(adata), True if ligand expressed in that spot. - R_bool: np.array len(R_bool)==len(adata), True if receptor expressed in that spot. - sig_bool np.array: len(sig_bool)==len(adata), True if spot has significant LR interactions. + adata : AnnData + Annotated data object containing spatial transcriptomics data. + L_bool : np.ndarray of bool, shape (n_spots,) + Boolean array indicating spots where the ligand is expressed. + R_bool : np.ndarray of bool, shape (n_spots,) + Boolean array indicating spots where the receptor is expressed. + sig_bool : np.ndarray of bool, shape (n_spots,) + Boolean array indicating spots with significant ligand-receptor interactions. + Returns ------- edge_list_unique: list> Either a list of tuples (directed), or @@ -266,7 +278,8 @@ def get_interaction_matrix( # 1) sig spot with ligand, only neighbours with receptor relevant # 2) sig spot with receptor, only neighbours with ligand relevant # NOTE, A<->B is double counted, but on different side of matrix. - # (if bidirectional interaction between two spots, counts as two seperate interactions). + # (if bidirectional interaction between two spots, counts as two seperate + # interactions). LR_edges = get_interactions( cell_data, neighbourhood_bcs, @@ -446,13 +459,15 @@ def count_grid( adata: AnnData The data object including the cell types to count num_row: int Number of grids on height num_col: int Number of grids on width - use_label: The cell type results to use in counting - use_het: The stoarge place for result - radius: int Distance to determine the neighbour grids (default: 1=nearest), radius=0 means within grid + use_label: The cell type results to use in counting + use_het: The storage place for result + radius: int Distance to determine the neighbour grids + (default: 1=nearest), radius=0 means within grid Returns ------- - adata: AnnData With the counts of specified clusters in each grid of the tissue stored as adata.uns['het'] + adata (AnnData): With the counts of specified clusters in each grid of the + tissue stored as adata.uns['het'] """ coor = adata.obs[["imagerow", "imagecol"]] diff --git a/stlearn/tools/microenv/cci/het_helpers.py b/stlearn/tools/microenv/cci/het_helpers.py index 270e811c..c1b76f9b 100644 --- a/stlearn/tools/microenv/cci/het_helpers.py +++ b/stlearn/tools/microenv/cci/het_helpers.py @@ -3,21 +3,19 @@ """ import numpy as np -import numba -from numba import types +from numba import njit from numba.typed import List -from numba import njit, jit @njit def edge_core( - cell_data: np.ndarray, - cell_type_index: int, - neighbourhood_bcs: List, - neighbourhood_indices: List, - spot_indices: np.array = None, - neigh_bool: np.array = None, - cutoff: float = 0.2, + cell_data: np.ndarray, + cell_type_index: int, + neighbourhood_bcs: List, + neighbourhood_indices: List, + spot_indices: np.array = None, + neigh_bool: np.array = None, + cutoff: float = 0.2, ) -> np.array: """Gets the edges which connect inputted spots to neighbours of a given cell type. @@ -32,7 +30,7 @@ def edge_core( cell_type_index: int Column of cell_data that contains the \ cell type of interest. - neighbourhood_bcs: List List of lists, inner list for each \ + neighbourhood_bcs (List): List of lists, inner list for each \ spot. First element of inner list is \ spot barcode, second element is array \ of neighbourhood spot barcodes. @@ -77,7 +75,7 @@ def edge_core( elif len(spot_indices) == 0: return edge_list[1:] - ### Within-spot mode + # Within-spot mode # within-spot, will have only itself as a neighbour in this mode within_mode = edge_list[0][0] == edge_list[0][1] if within_mode: @@ -86,7 +84,7 @@ def edge_core( if neigh_bool[i] and cell_data[i] > cutoff: edge_list.append((neighbourhood_bcs[i][0], neighbourhood_bcs[i][1][0])) - ### Between-spot mode + # Between-spot mode else: # Subsetting the neighbourhoods to relevant spots # neighbourhood_bcs_sub = List() @@ -130,12 +128,12 @@ def init_edge_list(neighbourhood_bcs): @njit def get_between_spot_edge_array( - edge_list: List, - neighbourhood_bcs: List, - neighbourhood_indices: List, - neigh_bool: np.array, - cell_data: np.array, - cutoff: float = 0, + edge_list: List, + neighbourhood_bcs: List, + neighbourhood_indices: List, + neigh_bool: np.array, + cell_data: np.array, + cutoff: float = 0, ): """ Populates edge_list with edges linking spots with a valid neighbour \ of a given cell type. Validity of neighbour determined by neigh_bool, \ @@ -186,7 +184,7 @@ def add_unique_edges(edge_list, edge_starts, edge_ends): edge_startj, edge_endj = edge_starts[j], edge_ends[j] # Direction doesn't matter # if (edge_start == edge_startj and edge_end == edge_endj) or ( - edge_end == edge_startj and edge_start == edge_endj + edge_end == edge_startj and edge_start == edge_endj ): edge_added[j] = True @@ -263,12 +261,12 @@ def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): # @njit def get_neighbourhoods_FAST( - spot_bcs: np.array, - spot_neigh_bcs: np.ndarray, - n_spots: int, - str_dtype: str, - neigh_indices: np.array, - neigh_bcs: np.array, + spot_bcs: np.array, + spot_neigh_bcs: np.ndarray, + n_spots: int, + str_dtype: str, + neigh_indices: np.array, + neigh_bcs: np.array, ): """Gets the neighbourhood information, njit compiled.""" @@ -277,12 +275,12 @@ def get_neighbourhoods_FAST( # neighbourhood_bcs = List((numba.int64, numba.int64[:])) # neighbourhood_indices = List( (types.unicode_type, types.unicode_type[:]) ) - ### Numba version + # Numba version # neighbours = List([neigh_indices])[1:] # neighbourhood_bcs = List() # neighbourhood_indices = List([(0, neigh_indices)])[1:] - #### Trying normal lists + # Trying normal lists neighbours, neighbourhood_bcs, neighbourhood_indices = [], [], [] for i in range(spot_neigh_bcs.shape[0]): @@ -297,12 +295,10 @@ def get_neighbourhoods_FAST( # neigh_bcs_array = np.empty(len(neigh_bcs_sub), dtype=str_dtype) # neigh_indices = np.zeros((len(neigh_bcs_sub)), dtype=np.int64) neigh_bcs_array, neigh_indices = [], [] - neigh_bcs_sub = List() for j, neigh_bc in enumerate(neigh_bcs): bc_indices = np.where(spot_bcs == neigh_bc)[0] if len(bc_indices) > 0: - neigh_bcs_array.append(neigh_bc) neigh_indices.append(bc_indices[0]) @@ -351,12 +347,12 @@ def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): def get_neighbourhoods_FAST( - spot_bcs: np.array, - spot_neigh_bcs: np.ndarray, - n_spots: int, - str_dtype: str, - neigh_indices: np.array, - neigh_bcs: np.array, + spot_bcs: np.array, + spot_neigh_bcs: np.ndarray, + n_spots: int, + str_dtype: str, + neigh_indices: np.array, + neigh_bcs: np.array, ): """Gets the neighbourhood information, njit compiled.""" @@ -368,7 +364,6 @@ def get_neighbourhoods_FAST( neigh_bcs = neigh_bcs[neigh_bcs != ""] neigh_bcs_array, neigh_indices = [], [] - neigh_bcs_sub = List() for j, neigh_bc in enumerate(neigh_bcs): bc_indices = np.where(spot_bcs == neigh_bc)[0] @@ -391,7 +386,7 @@ def get_neighbourhoods(adata): # Old stlearn version where didn't store neighbourhood barcodes, not good # for anndata subsetting!! - if not "spot_neigh_bcs" in adata.obsm: + if "spot_neigh_bcs" not in adata.obsm: # Determining the neighbour spots used for significance testing # neighbours = List() for i in range(adata.obsm["spot_neighbours"].shape[0]): diff --git a/stlearn/tools/microenv/cci/merge.py b/stlearn/tools/microenv/cci/merge.py index 6f25908b..15017fe1 100644 --- a/stlearn/tools/microenv/cci/merge.py +++ b/stlearn/tools/microenv/cci/merge.py @@ -1,13 +1,12 @@ import numpy as np -import pandas as pd from anndata import AnnData def merge( - adata: AnnData, - use_lr: str = "cci_lr", - use_het: str = "cci_het", - verbose: bool = True, + adata: AnnData, + use_lr: str = "cci_lr", + use_het: str = "cci_het", + verbose: bool = True, ) -> AnnData: """Merge results from cell type heterogeneity and L-R cluster Parameters @@ -25,7 +24,8 @@ def merge( if verbose: print( - "Results of spatial interaction analysis has been written to adata.uns['merged']" + "Results of spatial interaction analysis has been written to " + + "adata.uns['merged']" ) return adata diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index 083ae1ef..b7d9ab61 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -1,10 +1,9 @@ import numpy as np import pandas as pd -from scipy.spatial.distance import euclidean, canberra -from sklearn.preprocessing import MinMaxScaler - from numba import njit, prange from numba.typed import List +from scipy.spatial.distance import canberra +from sklearn.preprocessing import MinMaxScaler from .base import get_lrs_scores @@ -13,7 +12,7 @@ def nonzero_quantile(expr, q, interpolation): """Calculating the non-zero quantiles.""" nonzero_expr = expr[expr > 0] quants = np.quantile(nonzero_expr, q=q, interpolation=interpolation) - if type(quants) != np.array and type(quants) != np.ndarray: + if quants is not np.array and quants is not np.ndarray: quants = np.array([quants]) return quants @@ -36,7 +35,9 @@ def get_lr_quants( """Gets the quantiles per gene in the LR pair, & then concatenates. Returns ------- - lr_quants, l_quants, r_quants: np.ndarray First is concatenation of two latter. Each row is a quantile value, each column is a LR pair. + lr_quants, l_quants, r_quants (np.ndarray): First is concatenation of two latter. + Each row is a quantile value, each + column is an LR pair. """ quant_func = nonzero_quantile if method != "quantiles" else np.quantile @@ -58,7 +59,9 @@ def get_lr_zeroprops(lr_expr: pd.DataFrame, l_indices: list, r_indices: list): """Gets the proportion of zeros per gene in the LR pair, & then concatenates. Returns ------- - lr_props, l_props, r_props: np.ndarray First is concatenation of two latter. Each row is a prop value, each column is a LR pair. + lr_props, l_props, r_props (np.ndarray): First is concatenation of two latter. + Each row is a prop value, each column + is an LR pair. """ # First getting the quantiles of gene expression # @@ -76,7 +79,8 @@ def get_lr_bounds(lr_value: float, bin_bounds: np.array): """For the given lr_value, returns the bin where it belongs. Returns ------- - lr_bin: tuple Tuple of length 2, first is the lower bound of the bin, second is upper bound of the bin. + lr_bin (tuple): Tuple of length 2, first is the lower bound of the bin, second + is upper bound of the bin. """ if np.any(bin_bounds == lr_value): # If sits on a boundary lr_i = np.where(bin_bounds == lr_value)[0][0] @@ -105,17 +109,17 @@ def get_similar_genes( by measuring distance between the gene expression quantiles. Parameters ---------- - ref_quants: np.array The pre-calculated quantiles. - ref_props: np.array The query zero proportions. - n_genes: int Number of equivalent genes to select. + ref_quants: np.array The pre-calculated quantiles. + ref_props: np.array The query zero proportions. + n_genes: int Number of equivalent genes to select. candidate_expr: np.ndarray Expression of gene candidates (cells*genes). candidate_genes: np.array Same as candidate_expr.shape[1], indicating gene names. - quantiles: tuple The quantile to use + quantiles: tuple The quantile to use Returns ------- similar_genes: np.array Array of strings for gene names. """ - if type(quantiles) == float: + if quantiles is float: quantiles = np.array([quantiles]) else: quantiles = np.array(quantiles) @@ -168,17 +172,21 @@ def get_similar_genes_Quantiles( by measuring distance between the gene expression quantiles. Parameters ---------- - gene_expr: np.array Expression of the gene of interest, or, if the same length as quantiles, then assumes is the pre-calculated quantiles. - n_genes: int Number of equivalent genes to select. - candidate_quants: np.ndarray Expression quantiles of gene candidates (quantiles*genes). - candidate_genes: np.array Same as candidate_expr.shape[1], indicating gene names. - quantiles: tuple The quantile to use + gene_expr: np.array Expression of the gene of interest, or, if the + same length as quantiles, then assumes is the + pre-calculated quantiles. + n_genes: int Number of equivalent genes to select. + candidate_quants: np.ndarray Expression quantiles of gene candidates + (quantiles*genes). + candidate_genes: np.array Same as candidate_expr.shape[1], indicating gene + names. + quantiles: tuple The quantile to use Returns ------- similar_genes: np.array Array of strings for gene names. """ - if type(quantiles) == float: + if quantiles is float: quantiles = np.array([quantiles]) else: quantiles = np.array(quantiles) @@ -295,7 +303,7 @@ def get_lr_features(adata, lr_expr, lrs, quantiles): # Calculating the zero proportions, for grouping based on median/zeros # lr_props, l_props, r_props = get_lr_zeroprops(lr_expr, l_indices, r_indices) - ######## Getting lr features for later diagnostics ####### + # Getting lr features for later diagnostics lr_meds, l_meds, r_meds = get_lr_quants( lr_expr, l_indices, r_indices, quantiles=np.array([0.5]), method="" ) diff --git a/stlearn/tools/microenv/cci/permutation.py b/stlearn/tools/microenv/cci/permutation.py index 6ca6ce12..6abebe31 100644 --- a/stlearn/tools/microenv/cci/permutation.py +++ b/stlearn/tools/microenv/cci/permutation.py @@ -1,34 +1,38 @@ -import sys, os, random, scipy +import os +import random +import sys + import numpy as np import pandas as pd -from numba.typed import List +import scipy import statsmodels.api as sm +from anndata import AnnData +from numba.typed import List +from sklearn.cluster import AgglomerativeClustering from statsmodels.stats.multitest import multipletests - from tqdm import tqdm -from sklearn.cluster import AgglomerativeClustering -from anndata import AnnData -from .base import lr, calc_neighbours, get_spot_lrs, get_lrs_scores, get_scores +from .base import calc_neighbours, get_lrs_scores, get_scores, get_spot_lrs, lr from .merge import merge -from .perm_utils import get_lr_features, get_lr_bg +from .perm_utils import get_lr_bg, get_lr_features # Newest method # def perform_spot_testing( - adata: AnnData, - lr_scores: np.ndarray, - lrs: np.array, - n_pairs: int, - neighbours: List, - het_vals: np.array, - min_expr: float, - adj_method: str = "fdr_bh", - pval_adj_cutoff: float = 0.05, - verbose: bool = True, - save_bg=False, - neg_binom=False, - quantiles=(0.5, 0.75, 0.85, 0.9, 0.95, 0.97, 0.98, 0.99, 0.995, 0.9975, 0.999, 1), + adata: AnnData, + lr_scores: np.ndarray, + lrs: np.array, + n_pairs: int, + neighbours: List, + het_vals: np.array, + min_expr: float, + adj_method: str = "fdr_bh", + pval_adj_cutoff: float = 0.05, + verbose: bool = True, + save_bg=False, + neg_binom=False, + quantiles=( + 0.5, 0.75, 0.85, 0.9, 0.95, 0.97, 0.98, 0.99, 0.995, 0.9975, 0.999, 1), ): """Calls significant spots by creating random gene pairs with similar expression to given LR pair; only generate background for spots @@ -55,7 +59,7 @@ def perform_spot_testing( ) return - ####### Quantiles to select similar gene to LRs to gen. rand-pairs ####### + # Quantiles to select similar gene to LRs to gen. rand-pairs lr_expr = adata[:, lr_genes].to_df() lr_feats = get_lr_features(adata, lr_expr, lrs, quantiles) l_quants = lr_feats.loc[ @@ -72,7 +76,7 @@ def perform_spot_testing( r_quants = r_quants.astype(" AnnData: """Permutation test for merged result Parameters @@ -356,16 +361,23 @@ def permutation( adata: AnnData The data object including the cell types to count n_pairs: int Number of gene pairs to run permutation test (default: 1000) distance: int Distance between spots (default: 30) - use_lr: str LR cluster used for permutation test (default: 'lr_neighbours_louvain_max') - use_het: str cell type diversity counts used for permutation test (default 'het') - neg_binom: bool Whether to fit neg binomial paramaters to bg distribution for p-val est. - adj_method: str Method used by statsmodels.stats.multitest.multipletests for MHT correction. - neighbours: list List of the neighbours for each spot, if None then computed. Useful for speeding up function. + use_lr: str LR cluster used for permutation test + (default: 'lr_neighbours_louvain_max') + use_het: str cell type diversity counts used for permutation test + (default 'het') + neg_binom: bool Whether to fit neg binomial parameters to bg distribution + for p-val est. + adj_method: str Method used by statsmodels.stats.multitest.multipletests + for MHT correction. + neighbours: list List of the neighbours for each spot, if None then + computed. Useful for speeding up function. **kwargs: Extra arguments parsed to lr. Returns ------- - adata: AnnData Data Frame of p-values from permutation test for each window stored in adata.uns['merged_pvalues'] - Final significant merged scores stored in adata.uns['merged_sign'] + adata: AnnData Data Frame of p-values from permutation test for each + window stored in adata.uns['merged_pvalues'] + Final significant merged scores stored in + adata.uns['merged_sign'] """ # blockPrint() @@ -374,7 +386,7 @@ def permutation( genes = get_valid_genes(adata, n_pairs) if len(adata.uns["lr"]) > 1: raise ValueError("Permutation test only supported for one LR pair scenario.") - elif type(bg_pairs) == type(None): + elif bg_pairs is None: pairs = get_rand_pairs(adata, genes, n_pairs, lrs=adata.uns["lr"]) else: pairs = bg_pairs @@ -383,11 +395,13 @@ def permutation( # generate random pairs lr1 = adata.uns['lr'][0].split('_')[0] lr2 = adata.uns['lr'][0].split('_')[1] - genes = [item for item in adata.var_names.tolist() if not (item.startswith('MT-') or item.startswith('MT_') or item==lr1 or item==lr2)] + genes = [item for item in adata.var_names.tolist() if not + (item.startswith('MT-') or item.startswith('MT_') or + item==lr1 or item==lr2)] random.shuffle(genes) pairs = [i + '_' + j for i, j in zip(genes[:n_pairs], genes[-n_pairs:])] """ - if use_het != None: + if use_het is not None: scores = adata.obsm["merged"] else: scores = adata.obsm[use_lr] @@ -396,12 +410,11 @@ def permutation( query_pair = adata.uns["lr"] # If neighbours not inputted, then compute # - if type(neighbours) == type(None): + if neighbours is None: neighbours = calc_neighbours(adata, distance, index=run_fast) - if not run_fast and type(background) == type( - None - ): # Run original way if 'fast'=False argument inputted. + if not run_fast and background is None: + # Run original way if 'fast'=False argument inputted. background = [] for item in pairs: adata.uns["lr"] = [item] @@ -413,19 +426,19 @@ def permutation( neighbours=neighbours, **kwargs, ) - if use_het != None: + if use_het is not None: merge(adata, use_lr=use_lr, use_het=use_het, verbose=False) background += adata.obsm["merged"].tolist() else: background += adata.obsm[use_lr].tolist() background = np.array(background) - elif type(background) == type(None): # Run fast if background not inputted + elif background is None: # Run fast if background not inputted spot_lr1s = get_spot_lrs(adata, pairs, lr_order=True, filter_pairs=False) spot_lr2s = get_spot_lrs(adata, pairs, lr_order=False, filter_pairs=False) het_vals = ( - np.array([1] * len(adata)) if use_het == None else adata.obsm[use_het] + np.array([1] * len(adata)) if use_het is None else adata.obsm[use_het] ) background = get_scores( spot_lr1s.values, spot_lr2s.values, neighbours, het_vals @@ -434,12 +447,12 @@ def permutation( # log back the original query adata.uns["lr"] = query_pair - #### Negative Binomial fit + # Negative Binomial fit pvals, pvals_adj, log10_pvals, lr_sign = get_stats( scores, background, neg_binom, adj_method ) - if use_het != None: + if use_het is not None: adata.obsm["merged"] = scores adata.obsm["merged_pvalues"] = log10_pvals adata.obsm["merged_sign"] = lr_sign @@ -463,13 +476,13 @@ def permutation( def get_stats( - scores: np.array, - background: np.array, - total_bg: int, - neg_binom: bool = False, - adj_method: str = "fdr_bh", - pval_adj_cutoff: float = 0.01, - return_negbinom_params: bool = False, + scores: np.array, + background: np.array, + total_bg: int, + neg_binom: bool = False, + adj_method: str = "fdr_bh", + pval_adj_cutoff: float = 0.01, + return_negbinom_params: bool = False, ): """Retrieves valid candidate genes to be used for random gene pairs. Parameters @@ -477,19 +490,23 @@ def get_stats( scores: np.array Per spot scores for a particular LR pair. background: np.array Background distribution for non-zero scores. total_bg: int Total number of background values calculated. - neg_binom: bool Whether to use neg-binomial distribution to estimate p-values, NOT appropriate with log1p data, alternative is to use background distribution itself (recommend higher number of n_pairs for this). - adj_method: str Parsed to statsmodels.stats.multitest.multipletests for multiple hypothesis testing correction. + neg_binom: bool Whether to use neg-binomial distribution to estimate + p-values, NOT appropriate with log1p data, alternative is + to use background distribution itself (recommend higher + number of n_pairs for this). + adj_method: str Parsed to statsmodels.stats.multitest.multipletests for + multiple hypothesis testing correction. Returns ------- - stats: tuple Per spot pvalues, pvals_adj, log10_pvals_adj, lr_sign (the LR scores for significant spots). + stats: tuple Per spot pvalues, pvals_adj, log10_pvals_adj, lr_sign + (the LR scores for significant spots). """ - ##### Negative Binomial fit + # Negative Binomial fit if neg_binom: # Need to make full background for fitting !!! background = np.array(list(background) + [0] * (total_bg - len(background))) - pmin, pmax = min(background), max(background) + pmin = min(background) background2 = [item - pmin for item in background] - x = np.linspace(pmin, pmax, 1000) res = sm.NegativeBinomial( background2, np.ones(len(background2)), loglike_method="nb2" ).fit(start_params=[0.1, 0.3], disp=0) @@ -497,7 +514,7 @@ def get_stats( mu = np.exp(res.params[0]) alpha = res.params[1] Q = 0 - size = 1.0 / alpha * mu**Q + size = 1.0 / alpha * mu ** Q prob = size / (size + mu) if return_negbinom_params: # For testing purposes # @@ -506,11 +523,12 @@ def get_stats( # Calculate probability for all spots pvals = 1 - scipy.stats.nbinom.cdf(scores - pmin, size, prob) - else: ###### Using the actual values to estimate p-values + else: + # Using the actual values to estimate p-values pvals = np.zeros((1, len(scores)), dtype=np.float)[0, :] nonzero_score_bool = scores > 0 nonzero_score_indices = np.where(nonzero_score_bool)[0] - zero_score_indices = np.where(nonzero_score_bool == False)[0] + zero_score_indices = np.where(nonzero_score_bool is False)[0] pvals[zero_score_indices] = (total_bg - len(background)) / total_bg pvals[nonzero_score_indices] = [ len(np.where(background >= scores[i])[0]) / total_bg @@ -557,31 +575,33 @@ def get_valid_genes(adata: AnnData, n_pairs: int) -> np.array: def get_rand_pairs( - adata: AnnData, - genes: np.array, - n_pairs: int, - lrs: list = None, - im: int = None, + adata: AnnData, + genes: np.array, + n_pairs: int, + lrs: list = None, + im: int = None, ): """Gets equivalent random gene pairs for the inputted lr pair. Parameters ---------- - adata: AnnData The data object including the cell types to count - lr: int The lr pair string to get equivalent random pairs for (e.g. 'L_R') - genes: np.array Candidate genes to use as pairs. - n_pairs: int Number of random pairs to generate. + adata (AnnData): The data object including the cell types to count + genes (np.array): Candidate genes to use as pairs. + n_pairs (int): Number of random pairs to generate. + lr (int): The lr pair string to get equivalent random pairs + for (e.g. 'L_R') Returns ------- - pairs: list List of random gene pairs with equivalent mean expression (e.g. ['L_R']) + pairs (list) List of random gene pairs with equivalent mean expression + (e.g. ['L_R']) """ lr_genes = [lr.split("_")[0] for lr in lrs] lr_genes += [lr.split("_")[1] for lr in lrs] # get the position of the median of the means between the two genes means_ordered, genes_ordered = get_ordered(adata, genes) - if type(im) == type(None): # Single background per lr pair mode - l, r = lrs[0].split("_") - im = get_median_index(l, r, means_ordered.values, genes_ordered) + if im is None: # Single background per lr pair mode + ligand, receptor = lrs[0].split("_") + im = get_median_index(ligand, receptor, means_ordered.values, genes_ordered) # get n_pair genes sorted by distance to im selected = ( @@ -590,7 +610,7 @@ def get_rand_pairs( .drop(lr_genes)[: n_pairs * 2] .index.tolist() ) - selected = selected[0 : n_pairs * 2] + selected = selected[0: n_pairs * 2] adata.uns["selected"] = selected # form gene pairs from selected randomly random.shuffle(selected) @@ -605,21 +625,23 @@ def get_ordered(adata, genes): return means_ordered, genes_ordered -def get_median_index(l, r, means_ordered, genes_ordered): - """ "Retrieves the index of the gene with a mean expression between the two genes in the lr pair. +def get_median_index(ligand, receptor, means_ordered, genes_ordered): + """ Retrieves the index of the gene with a mean expression between the two genes + in the lr pair. Parameters ---------- - X: np.ndarray Spot*Gene expression. - l: str Ligand gene. - r: str Receptor gene. - genes: np.array Candidate genes to use as pairs. + ligand: Ligand gene. + receptor: Receptor gene. + genes_ordered: + means_ordered: Returns ------- - pairs: list List of random gene pairs with equivalent mean expression (e.g. ['L_R']) + pairs (list): List of random gene pairs with equivalent mean expression + (e.g. ['L_R']) """ # sort the mean of each gene expression - i1 = np.where(genes_ordered == l)[0][0] - i2 = np.where(genes_ordered == r)[0][0] + i1 = np.where(genes_ordered == ligand)[0][0] + i2 = np.where(genes_ordered == receptor)[0][0] if means_ordered[i1] > means_ordered[i2]: it = i1 i1 = i2 diff --git a/stlearn/utils.py b/stlearn/utils.py index ba9e1280..b658fe26 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -1,13 +1,10 @@ -import numpy as np -from anndata import AnnData -import networkx as nx - -from typing import Optional, Union, Mapping # Special -from typing import Tuple # Classes - +from collections.abc import Mapping +from enum import Enum from textwrap import dedent -from enum import Enum +import networkx as nx +import numpy as np +from anndata import AnnData from matplotlib import axes from matplotlib.axes import Axes @@ -24,7 +21,7 @@ class _AxesSubplot(Axes, axes.SubplotBase): def _check_spot_size( - spatial_data: Optional[Mapping], spot_size: Optional[float] + spatial_data: Mapping | None, spot_size: float | None ) -> float: """ Resolve spot_size value. @@ -42,9 +39,9 @@ def _check_spot_size( def _check_scale_factor( - spatial_data: Optional[Mapping], - img_key: Optional[str], - scale_factor: Optional[float], + spatial_data: Mapping | None, + img_key: str | None, + scale_factor: float | None, ) -> float: """Resolve scale_factor, defaults to 1.""" if scale_factor is not None: @@ -56,8 +53,8 @@ def _check_scale_factor( def _check_spatial_data( - uns: Mapping, library_id: Union[Empty, None, str] -) -> Tuple[Optional[str], Optional[Mapping]]: + uns: Mapping, library_id: Empty | None | str +) -> tuple[str | None, Mapping | None]: """ Given a mapping, try and extract a library id/ mapping with spatial data. Assumes this is `.uns` from how we parse visium data. @@ -81,11 +78,11 @@ def _check_spatial_data( def _check_img( - spatial_data: Optional[Mapping], - img: Optional[np.ndarray], - img_key: Union[None, str, Empty], + spatial_data: Mapping | None, + img: np.ndarray | None, + img_key: None | str | Empty, bw: bool = False, -) -> Tuple[Optional[np.ndarray], Optional[str]]: +) -> tuple[np.ndarray | None, str | None]: """ Resolve image for spatial plots. """ @@ -101,8 +98,8 @@ def _check_img( def _check_coords( - obsm: Optional[Mapping], scale_factor: Optional[float] -) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]: + obsm: Mapping | None, scale_factor: float | None +) -> tuple[np.ndarray | None, np.ndarray | None]: image_coor = obsm["spatial"] * scale_factor imagecol = image_coor[:, 0] imagerow = image_coor[:, 1] @@ -110,7 +107,7 @@ def _check_coords( return [imagecol, imagerow] -def _read_graph(adata: AnnData, graph_type: Optional[str]): +def _read_graph(adata: AnnData, graph_type: str | None): if graph_type == "PTS_graph": graph = nx.from_scipy_sparse_array( adata.uns[graph_type]["graph"], create_using=nx.DiGraph diff --git a/stlearn/wrapper/concatenate_spatial_adata.py b/stlearn/wrapper/concatenate_spatial_adata.py index c5d1ae07..cc9273d7 100644 --- a/stlearn/wrapper/concatenate_spatial_adata.py +++ b/stlearn/wrapper/concatenate_spatial_adata.py @@ -121,7 +121,7 @@ def concatenate_spatial_adata(adata_list, ncols=2, fixed_size=(2000, 2000)): for min_id in range(0, len(use_adata_list), ncols): img_row = np.hstack(imgs[min_id : min_id + ncols]) img_rows.append(img_row) - imgs_comb = np.vstack((i for i in img_rows)) + imgs_comb = np.vstack(i for i in img_rows) adata_concat = use_adata_list[0].concatenate(use_adata_list[1:]) adata_concat.uns["spatial"] = use_adata_list[0].uns["spatial"] diff --git a/stlearn/wrapper/convert_scanpy.py b/stlearn/wrapper/convert_scanpy.py index e384f0a0..2f11e047 100644 --- a/stlearn/wrapper/convert_scanpy.py +++ b/stlearn/wrapper/convert_scanpy.py @@ -1,11 +1,11 @@ -from typing import Optional + from anndata import AnnData def convert_scanpy( adata: AnnData, use_quality: str = "hires", -) -> Optional[AnnData]: +) -> AnnData | None: adata.var_names_make_unique() diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 9db3dc25..030e7d82 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -1,31 +1,33 @@ """Reading and Writing""" +import json +import logging as logg from pathlib import Path -from typing import Optional, Union -from anndata import AnnData + +import matplotlib.pyplot as plt import numpy as np -from PIL import Image import pandas as pd -import stlearn -from .._compat import Literal import scanpy -import matplotlib.pyplot as plt +from anndata import AnnData from matplotlib.image import imread -import json -import logging as logg +from PIL import Image + +import stlearn + +from .._compat import Literal _QUALITY = Literal["fulres", "hires", "lowres"] _background = ["black", "white"] def Read10X( - path: Union[str, Path], - genome: Optional[str] = None, + path: str | Path, + genome: str | None = None, count_file: str = "filtered_feature_bc_matrix.h5", library_id: str = None, - load_images: Optional[bool] = True, + load_images: bool | None = True, quality: _QUALITY = "hires", - image_path: Union[str, Path] = None, + image_path: str | Path = None, ) -> AnnData: """\ Read Visium data from 10X (wrap read_visium from scanpy) @@ -35,23 +37,26 @@ def Read10X( coordinates and scale factors. Based on the `Space Ranger output docs`_. - .. _Space Ranger output docs: https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview + _Space Ranger output docs: + https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview Parameters ---------- path - Path to directory for visium datafiles. + The path to directory for Visium datafiles. genome Filter expression to genes within this genome. count_file - Which file in the passed directory to use as the count file. Typically would be one of: - 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. + Which file in the directory to use as the count file. Typically, it would be one + of: 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. + Identifier for the Visium library. Can be modified when concatenating multiple + adata objects. load_images Load image or not. quality - Set quality that convert to stlearn to use. Store in anndata.obs['imagecol' & 'imagerow'] + Set quality that convert to stlearn to use. Store in + anndata.obs['imagecol' & 'imagerow'] image_path Path to image. Only need when loading full resolution image. @@ -200,9 +205,9 @@ def Read10X( def ReadOldST( - count_matrix_file: Union[str, Path] = None, - spatial_file: Union[str, Path] = None, - image_file: Union[str, Path] = None, + count_matrix_file: str | Path = None, + spatial_file: str | Path = None, + image_file: str | Path = None, library_id: str = "OldST", scale: float = 1.0, quality: str = "hires", @@ -216,15 +221,17 @@ def ReadOldST( count_matrix_file Path to count matrix file. spatial_file - Path to spatial location file. + Path to the spatial location file. image_file Path to the tissue image file library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. + Identifier for the Visium library. Can be modified when concatenating multiple + adata objects. scale Set scale factor. quality - Set quality that convert to stlearn to use. Store in anndata.obs['imagecol' & 'imagerow'] + Set quality that convert to stlearn to use. Store in + anndata.obs['imagecol' & 'imagerow'] spot_diameter_fullres Diameter of spot in full resolution @@ -248,8 +255,8 @@ def ReadOldST( def ReadSlideSeq( - count_matrix_file: Union[str, Path], - spatial_file: Union[str, Path], + count_matrix_file: str | Path, + spatial_file: str | Path, library_id: str = None, scale: float = None, quality: str = "hires", @@ -264,17 +271,19 @@ def ReadSlideSeq( count_matrix_file Path to count matrix file. spatial_file - Path to spatial location file. + Path to the spatial location file. library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. + Identifier for the Visium library. Can be modified when concatenating + multiple adata objects. scale Set scale factor. quality - Set quality that convert to stlearn to use. Store in anndata.obs['imagecol' & 'imagerow'] + Set quality that convert to stlearn to use. Store in + anndata.obs['imagecol' & 'imagerow'] spot_diameter_fullres Diameter of spot in full resolution background_color - Color of the backgound. Only `black` or `white` is allowed. + Color of the background. Only `black` or `white` is allowed. Returns ------- @@ -290,7 +299,7 @@ def ReadSlideSeq( adata.obs["index"] = meta["index"].values - if scale == None: + if scale is None: max_coor = np.max(meta[["x", "y"]].values) scale = 2000 / max_coor @@ -329,8 +338,8 @@ def ReadSlideSeq( def ReadMERFISH( - count_matrix_file: Union[str, Path], - spatial_file: Union[str, Path], + count_matrix_file: str | Path, + spatial_file: str | Path, library_id: str = None, scale: float = None, quality: str = "hires", @@ -345,17 +354,19 @@ def ReadMERFISH( count_matrix_file Path to count matrix file. spatial_file - Path to spatial location file. + Path to the spatial location file. library_id - Identifier for the visium library. Can be modified when concatenating multiple adata objects. + Identifier for the Visium library. Can be modified when concatenating + multiple adata objects. scale Set scale factor. quality - Set quality that convert to stlearn to use. Store in anndata.obs['imagecol' & 'imagerow'] + Set quality that convert to stlearn to use. Store in + anndata.obs['imagecol' & 'imagerow'] spot_diameter_fullres Diameter of spot in full resolution background_color - Color of the backgound. Only `black` or `white` is allowed. + Color of the background. Only `black` or `white` is allowed. Returns ------- @@ -410,8 +421,8 @@ def ReadMERFISH( def ReadSeqFish( - count_matrix_file: Union[str, Path], - spatial_file: Union[str, Path], + count_matrix_file: str | Path, + spatial_file: str | Path, library_id: str = None, scale: float = 1.0, quality: str = "hires", @@ -441,7 +452,7 @@ def ReadSeqFish( spot_diameter_fullres Diameter of spot in full resolution background_color - Color of the backgound. Only `black` or `white` is allowed. + Color of the background. Only `black` or `white` is allowed. Returns ------- AnnData @@ -498,9 +509,9 @@ def ReadSeqFish( def ReadXenium( - feature_cell_matrix_file: Union[str, Path], - cell_summary_file: Union[str, Path], - image_path: Optional[Path] = None, + feature_cell_matrix_file: str | Path, + cell_summary_file: str | Path, + image_path: Path | None = None, library_id: str = None, scale: float = 1.0, quality: str = "hires", @@ -529,7 +540,7 @@ def ReadXenium( spot_diameter_fullres Diameter of spot in full resolution background_color - Color of the backgound. Only `black` or `white` is allowed. + Color of the background. Only `black` or `white` is allowed. Returns ------- AnnData @@ -594,7 +605,7 @@ def create_stlearn( count: pd.DataFrame, spatial: pd.DataFrame, library_id: str, - image_path: Optional[Path] = None, + image_path: Path | None = None, scale: float = None, quality: str = "hires", spot_diameter_fullres: float = 50, @@ -620,7 +631,7 @@ def create_stlearn( spot_diameter_fullres Diameter of spot in full resolution background_color - Color of the backgound. Only `black` or `white` is allowed. + Color of the background. Only `black` or `white` is allowed. Returns ------- AnnData diff --git a/tests/test_CCI.py b/tests/test_CCI.py index e31e887f..720f0181 100644 --- a/tests/test_CCI.py +++ b/tests/test_CCI.py @@ -8,10 +8,9 @@ from numba.typed import List import stlearn as st -from tests.utils import read_test_data - -import stlearn.tools.microenv.cci.het_helpers as het_hs import stlearn.tools.microenv.cci.het as het +import stlearn.tools.microenv.cci.het_helpers as het_hs +from tests.utils import read_test_data global adata adata = read_test_data() diff --git a/tests/test_PSTS.py b/tests/test_PSTS.py index 1a6b7676..21089a6a 100644 --- a/tests/test_PSTS.py +++ b/tests/test_PSTS.py @@ -5,10 +5,12 @@ import unittest -import stlearn as st +import numpy as np import scanpy as sc + +import stlearn as st + from .utils import read_test_data -import numpy as np global adata adata = read_test_data() diff --git a/tests/test_SME.py b/tests/test_SME.py index 96eec81f..a1f38200 100644 --- a/tests/test_SME.py +++ b/tests/test_SME.py @@ -5,8 +5,10 @@ import unittest -import stlearn as st import scanpy as sc + +import stlearn as st + from .utils import read_test_data global adata diff --git a/tests/utils.py b/tests/utils.py index 6a5cc78f..98482f96 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,8 @@ import os + +import numpy as np import scanpy as sc from PIL import Image -import numpy as np def read_test_data(): diff --git a/tox.ini b/tox.ini index 473ed981..76e1b229 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] requires = tox>=4 -env_list = lint, type, 3.1{3,2,1,0}, flake8 +env_list = lint, type, 3.1{3,2,1,0}, ruff [testenv:lint] description = run linters @@ -17,11 +17,13 @@ deps = commands = mypy {posargs:stlearn tests} -[testenv:flake8] -description = run flake8 linting +[testenv:ruff] +description = run ruff linting and formatting skip_install = true -deps = flake8 -commands = flake8 stlearn tests +deps = ruff +commands = + ruff check stlearn tests + ruff format --check stlearn tests [testenv] setenv = @@ -29,8 +31,3 @@ setenv = deps = pytest commands = pytest {posargs} - -[flake8] -max-line-length = 88 -extend-ignore = E203, W503 -exclude = .git,__pycache__,build,dist \ No newline at end of file From 27e081de90a6179409b0403fe51b3914a64c2d76 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 4 Jun 2025 09:10:37 +1000 Subject: [PATCH 026/241] More style issues. --- CONTRIBUTING.rst | 2 +- stlearn/_settings.py | 62 +-- stlearn/adds/add_labels.py | 12 +- stlearn/adds/add_lr.py | 12 +- stlearn/adds/annotation.py | 1 - stlearn/app/source/forms/forms.py | 17 +- stlearn/app/source/forms/utils.py | 1 + stlearn/app/source/forms/view_helpers.py | 1 - stlearn/classes.py | 1 - stlearn/em.py | 1 - stlearn/embedding/diffmap.py | 4 +- stlearn/embedding/fa.py | 1 - stlearn/embedding/ica.py | 5 +- stlearn/embedding/pca.py | 5 +- stlearn/embedding/umap.py | 29 +- .../image_preprocessing/feature_extractor.py | 1 - stlearn/image_preprocessing/image_tiling.py | 4 +- stlearn/image_preprocessing/segmentation.py | 1 - stlearn/pl.py | 1 - stlearn/plotting/QC_plot.py | 1 - stlearn/plotting/cci_plot.py | 405 ++++++++------- stlearn/plotting/cci_plot_helpers.py | 166 +++--- stlearn/plotting/classes.py | 477 +++++++++--------- stlearn/plotting/classes_bokeh.py | 40 +- stlearn/plotting/cluster_plot.py | 78 +-- stlearn/plotting/deconvolution_plot.py | 49 +- stlearn/plotting/feat_plot.py | 58 +-- stlearn/plotting/gene_plot.py | 60 +-- stlearn/plotting/mask_plot.py | 29 +- stlearn/plotting/non_spatial_plot.py | 5 +- stlearn/plotting/stack_3d_plot.py | 19 +- stlearn/plotting/subcluster_plot.py | 52 +- .../plotting/trajectory/DE_transition_plot.py | 12 +- .../plotting/trajectory/check_trajectory.py | 33 +- stlearn/plotting/trajectory/local_plot.py | 37 +- .../plotting/trajectory/pseudotime_plot.py | 51 +- .../trajectory/transition_markers_plot.py | 12 +- stlearn/plotting/trajectory/tree_plot.py | 35 +- .../plotting/trajectory/tree_plot_simple.py | 35 +- stlearn/plotting/utils.py | 10 +- stlearn/preprocessing/filter_genes.py | 13 +- stlearn/preprocessing/graph.py | 20 +- stlearn/preprocessing/log_scale.py | 19 +- stlearn/preprocessing/normalize.py | 16 +- stlearn/spatials/SME/_weighting_matrix.py | 1 - stlearn/spatials/SME/impute.py | 36 +- stlearn/spatials/SME/normalize.py | 11 +- stlearn/spatials/clustering/localization.py | 11 +- stlearn/spatials/morphology/adjust.py | 1 - stlearn/spatials/smooth/disk.py | 17 +- .../trajectory/detect_transition_markers.py | 28 +- stlearn/spatials/trajectory/global_level.py | 24 +- stlearn/spatials/trajectory/local_level.py | 19 +- stlearn/spatials/trajectory/pseudotime.py | 41 +- .../spatials/trajectory/pseudotimespace.py | 29 +- stlearn/spatials/trajectory/set_root.py | 4 +- stlearn/spatials/trajectory/utils.py | 28 +- .../trajectory/weight_optimization.py | 38 +- stlearn/tools/clustering/kmeans.py | 25 +- stlearn/tools/clustering/louvain.py | 24 +- stlearn/tools/label/label.py | 55 +- stlearn/tools/microenv/cci/analysis.py | 4 +- stlearn/tools/microenv/cci/base.py | 103 ++-- stlearn/tools/microenv/cci/base_grouping.py | 68 +-- stlearn/tools/microenv/cci/het_helpers.py | 52 +- stlearn/tools/microenv/cci/merge.py | 12 +- stlearn/tools/microenv/cci/permutation.py | 127 +++-- stlearn/utils.py | 4 +- stlearn/wrapper/convert_scanpy.py | 1 - 69 files changed, 1307 insertions(+), 1349 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index fbc922f4..40cc4228 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -88,7 +88,7 @@ Ready to contribute? Here's how to set up `stlearn` for local development. $ black stlearn tests $ flake8 stlearn tests - $ mypy stlearn + $ mypy stlearn tests $ pytest Or run everything with tox:: diff --git a/stlearn/_settings.py b/stlearn/_settings.py index 91d8b617..b3c06afa 100644 --- a/stlearn/_settings.py +++ b/stlearn/_settings.py @@ -62,32 +62,32 @@ def _type_check(var: Any, varname: str, types: type | tuple[type, ...]): raise TypeError(f"{varname} must be of type {possible_types_str}") -class stLearnConfig: # noqa N801 +class stLearnConfig: # noqa N801 """\ Config manager for scanpy. """ def __init__( - self, - *, - verbosity: str = "warning", - plot_suffix: str = "", - file_format_data: str = "h5ad", - file_format_figs: str = "pdf", - autosave: bool = False, - autoshow: bool = True, - writedir: str | Path = "./write/", - cachedir: str | Path = "./cache/", - datasetdir: str | Path = "./data/", - figdir: str | Path = "./figures/", - cache_compression: str | None = "lzf", - max_memory=15, - n_jobs=1, - logfile: str | Path | None = None, - categories_to_ignore: Iterable[str] = ("N/A", "dontknow", "no_gate", "?"), - _frameon: bool = True, - _vector_friendly: bool = False, - _low_resolution_warning: bool = True, + self, + *, + verbosity: str = "warning", + plot_suffix: str = "", + file_format_data: str = "h5ad", + file_format_figs: str = "pdf", + autosave: bool = False, + autoshow: bool = True, + writedir: str | Path = "./write/", + cachedir: str | Path = "./cache/", + datasetdir: str | Path = "./data/", + figdir: str | Path = "./figures/", + cache_compression: str | None = "lzf", + max_memory=15, + n_jobs=1, + logfile: str | Path | None = None, + categories_to_ignore: Iterable[str] = ("N/A", "dontknow", "no_gate", "?"), + _frameon: bool = True, + _vector_friendly: bool = False, + _low_resolution_warning: bool = True, ): # logging self._root_logger = _RootLogger(logging.INFO) # level will be replaced @@ -399,16 +399,16 @@ def categories_to_ignore(self, categories_to_ignore: Iterable[str]): ] def set_figure_params( - self, - dpi: int = 80, - dpi_save: int = 150, - frameon: bool = True, - vector_friendly: bool = True, - fontsize: int = 14, - color_map: str | None = None, - format: _Format = "pdf", - transparent: bool = False, - ipython_format: str = "png2x", + self, + dpi: int = 80, + dpi_save: int = 150, + frameon: bool = True, + vector_friendly: bool = True, + fontsize: int = 14, + color_map: str | None = None, + format: _Format = "pdf", + transparent: bool = False, + ipython_format: str = "png2x", ): """\ Set resolution/size, styling and format of figures. diff --git a/stlearn/adds/add_labels.py b/stlearn/adds/add_labels.py index cb7210bf..5b0875f7 100644 --- a/stlearn/adds/add_labels.py +++ b/stlearn/adds/add_labels.py @@ -5,12 +5,12 @@ def labels( - adata: AnnData, - label_filepath: str = None, - index_col: int = 0, - use_label: str = None, - sep: str = "\t", - copy: bool = False, + adata: AnnData, + label_filepath: str = None, + index_col: int = 0, + use_label: str = None, + sep: str = "\t", + copy: bool = False, ) -> AnnData | None: """\ Add label transfer results into AnnData object diff --git a/stlearn/adds/add_lr.py b/stlearn/adds/add_lr.py index bafc45ea..d979a2ac 100644 --- a/stlearn/adds/add_lr.py +++ b/stlearn/adds/add_lr.py @@ -3,11 +3,11 @@ def lr( - adata: AnnData, - db_filepath: str = None, - sep: str = "\t", - source: str = "connectomedb", - copy: bool = False, + adata: AnnData, + db_filepath: str = None, + sep: str = "\t", + source: str = "connectomedb", + copy: bool = False, ) -> AnnData | None: """Add significant Ligand-Receptor pairs into AnnData object @@ -44,7 +44,7 @@ def lr( elif source == "connectomedb": ctdb = pd.read_csv(db_filepath, sep=sep, quotechar='"', encoding="latin1") adata.uns["lr"] = ( - ctdb["Ligand gene symbol"] + "_" + ctdb["Receptor gene symbol"] + ctdb["Ligand gene symbol"] + "_" + ctdb["Receptor gene symbol"] ).values.tolist() print("connectomedb results added to adata.uns['ctdb']") print("Added ligand receptor pairs to adata.uns['lr'].") diff --git a/stlearn/adds/annotation.py b/stlearn/adds/annotation.py index fcd6fe52..809c0cea 100644 --- a/stlearn/adds/annotation.py +++ b/stlearn/adds/annotation.py @@ -1,4 +1,3 @@ - from anndata import AnnData diff --git a/stlearn/app/source/forms/forms.py b/stlearn/app/source/forms/forms.py index 91aff56e..790bc97e 100644 --- a/stlearn/app/source/forms/forms.py +++ b/stlearn/app/source/forms/forms.py @@ -197,13 +197,13 @@ def getCCIForm(adata): related to CCI analysis. """ elements = [ - "Cell information (only discrete labels available, unless mixture already in " + - "anndata.uns)", + "Cell information (only discrete labels available, unless mixture already in " + + "anndata.uns)", "Minimum spots for LR to be considered", - "Spot mixture (only if the 'Cell Information' label selected available in " + - "anndata.uns)", - "Cell proportion cutoff (value above which cell is considered in spot " + - "if 'Spot mixture' selected)", + "Spot mixture (only if the 'Cell Information' label selected available in " + + "anndata.uns)", + "Cell proportion cutoff (value above which cell is considered in spot " + + "if 'Spot mixture' selected)", "Permutations (recommend atleast 1000)", ] element_fields = [ @@ -217,9 +217,7 @@ def getCCIForm(adata): fields = [] mix = False else: - fields = [ - key for key in adata.obs.keys() if adata.obs[key].values[0] is str - ] + fields = [key for key in adata.obs.keys() if adata.obs[key].values[0] is str] mix = fields[0] in adata.uns.keys() element_values = [fields, 20, mix, 0.2, 100] return createSuperForm(elements, element_fields, element_values) @@ -324,6 +322,7 @@ def getDEAForm(list_labels, methods): element_values = [list_labels, methods] return createSuperForm(elements, element_fields, element_values) + ######################## Junk Code ############################################# # def getCCIForm(step_log): # """ Gets the CCI form generated from the superform above. diff --git a/stlearn/app/source/forms/utils.py b/stlearn/app/source/forms/utils.py index 43284e68..1d5d0c75 100644 --- a/stlearn/app/source/forms/utils.py +++ b/stlearn/app/source/forms/utils.py @@ -1,4 +1,5 @@ """Helper utilities and decorators.""" + from flask import flash diff --git a/stlearn/app/source/forms/view_helpers.py b/stlearn/app/source/forms/view_helpers.py index a5613b61..3c2de3d0 100644 --- a/stlearn/app/source/forms/view_helpers.py +++ b/stlearn/app/source/forms/view_helpers.py @@ -1,7 +1,6 @@ """Helper functions for views.py.""" - def getVal(form, element): return getattr(form, element).data diff --git a/stlearn/classes.py b/stlearn/classes.py index f9ef77c2..12c25ede 100644 --- a/stlearn/classes.py +++ b/stlearn/classes.py @@ -4,7 +4,6 @@ Date: 20 Feb 2021 """ - import numpy as np from anndata import AnnData diff --git a/stlearn/em.py b/stlearn/em.py index 6768d1c6..d16c7bec 100644 --- a/stlearn/em.py +++ b/stlearn/em.py @@ -1,2 +1 @@ - # from .embedding.scvi import run_ldvae diff --git a/stlearn/embedding/diffmap.py b/stlearn/embedding/diffmap.py index 93a5c480..93338007 100644 --- a/stlearn/embedding/diffmap.py +++ b/stlearn/embedding/diffmap.py @@ -37,8 +37,8 @@ def run_diffmap(adata: AnnData, n_comps: int = 15, copy: bool = False): scanpy.tl.diffmap(adata, n_comps=n_comps, copy=copy) print( - "Diffusion Map is done! Generated in adata.obsm['X_diffmap'] and " + - "adata.uns['diffmap_evals']" + "Diffusion Map is done! Generated in adata.obsm['X_diffmap'] and " + + "adata.uns['diffmap_evals']" ) return adata if copy else None diff --git a/stlearn/embedding/fa.py b/stlearn/embedding/fa.py index a707efb3..b982c3d8 100644 --- a/stlearn/embedding/fa.py +++ b/stlearn/embedding/fa.py @@ -1,4 +1,3 @@ - from anndata import AnnData from scipy.sparse import issparse from sklearn.decomposition import FactorAnalysis diff --git a/stlearn/embedding/ica.py b/stlearn/embedding/ica.py index e99e77ca..5b990788 100644 --- a/stlearn/embedding/ica.py +++ b/stlearn/embedding/ica.py @@ -1,4 +1,3 @@ - from anndata import AnnData from scipy.sparse import issparse from sklearn.decomposition import FastICA @@ -62,8 +61,8 @@ def my_g(x): adata.uns["ica"] = {"params": {"n_factors": n_factors, "fun": fun, "tol": tol}} print( - "ICA is done! Generated in adata.obsm['X_ica'] and parameters in " + - "adata.uns['ica']" + "ICA is done! Generated in adata.obsm['X_ica'] and parameters in " + + "adata.uns['ica']" ) return adata if copy else None diff --git a/stlearn/embedding/pca.py b/stlearn/embedding/pca.py index d4b66f15..22ae94fb 100644 --- a/stlearn/embedding/pca.py +++ b/stlearn/embedding/pca.py @@ -1,4 +1,3 @@ - import numpy as np import scanpy from anndata import AnnData @@ -99,6 +98,6 @@ def run_pca( ) print( - "PCA is done! Generated in adata.obsm['X_pca'], adata.uns['pca'] and " + - "adata.varm['PCs']" + "PCA is done! Generated in adata.obsm['X_pca'], adata.uns['pca'] and " + + "adata.varm['PCs']" ) diff --git a/stlearn/embedding/umap.py b/stlearn/embedding/umap.py index 85f6e8b1..9b375a80 100644 --- a/stlearn/embedding/umap.py +++ b/stlearn/embedding/umap.py @@ -1,4 +1,3 @@ - import numpy as np import scanpy from anndata import AnnData @@ -10,20 +9,20 @@ def run_umap( - adata: AnnData, - min_dist: float = 0.5, - spread: float = 1.0, - n_components: int = 2, - maxiter: int | None = None, - alpha: float = 1.0, - gamma: float = 1.0, - negative_sample_rate: int = 5, - init_pos: _InitPos | np.ndarray | None = "spectral", - random_state: int | RandomState | None = 0, - a: float | None = None, - b: float | None = None, - copy: bool = False, - method: Literal["umap", "rapids"] = "umap", # noqa: F821 + adata: AnnData, + min_dist: float = 0.5, + spread: float = 1.0, + n_components: int = 2, + maxiter: int | None = None, + alpha: float = 1.0, + gamma: float = 1.0, + negative_sample_rate: int = 5, + init_pos: _InitPos | np.ndarray | None = "spectral", + random_state: int | RandomState | None = 0, + a: float | None = None, + b: float | None = None, + copy: bool = False, + method: Literal["umap", "rapids"] = "umap", # noqa: F821 ) -> AnnData | None: """\ Wrap function scanpy.pp.umap diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index 0d451041..75e3a2a2 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -1,4 +1,3 @@ - import numpy as np import pandas as pd from anndata import AnnData diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index 4daee2a8..ee338816 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -91,9 +91,7 @@ def tiling( tile.save(out_tile, "PNG") if verbose: - print( - f"generate tile at location ({str(imagecol)}, {str(imagerow)})" - ) + print(f"generate tile at location ({str(imagecol)}, {str(imagerow)})") pbar.update(1) diff --git a/stlearn/image_preprocessing/segmentation.py b/stlearn/image_preprocessing/segmentation.py index 8c7f4dfe..6d975aab 100644 --- a/stlearn/image_preprocessing/segmentation.py +++ b/stlearn/image_preprocessing/segmentation.py @@ -1,4 +1,3 @@ - import histomicstk as htk import numpy as np import scipy as sp diff --git a/stlearn/pl.py b/stlearn/pl.py index 54be1ef2..a41cb8d6 100644 --- a/stlearn/pl.py +++ b/stlearn/pl.py @@ -1,2 +1 @@ - # from .plotting.cci_plot import het_plot_interactive diff --git a/stlearn/plotting/QC_plot.py b/stlearn/plotting/QC_plot.py index 9b4af383..ebe0faa6 100644 --- a/stlearn/plotting/QC_plot.py +++ b/stlearn/plotting/QC_plot.py @@ -1,4 +1,3 @@ - import numpy as np from anndata import AnnData from matplotlib import pyplot as plt diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index a2816a37..4cc3467f 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -3,7 +3,7 @@ import sys from typing import ( Optional, # Special - ) +) import matplotlib import matplotlib.patches as patches @@ -42,14 +42,14 @@ def lr_diagnostics( - adata, - highlight_lrs: list = None, - n_top: int = None, - color0: str = "turquoise", - color1: str = "plum", - figsize: tuple = (10, 4), - lr_text_fp: dict = None, - show: bool = True, + adata, + highlight_lrs: list = None, + n_top: int = None, + color0: str = "turquoise", + color1: str = "plum", + figsize: tuple = (10, 4), + lr_text_fp: dict = None, + show: bool = True, ): """Diagnostic plot looking at relationship between technical features of lrs and lr rank. Two plots generated: left is the average of the median for nonzero @@ -107,17 +107,17 @@ def lr_diagnostics( def lr_summary( - adata, - n_top: int = 50, - highlight_lrs: list = None, - y: str = "n_spots_sig", - color: str = "gold", - figsize: tuple = None, - highlight_color: str = "red", - max_text: int = 50, - lr_text_fp: dict = None, - ax: Axes = None, - show: bool = True, + adata, + n_top: int = 50, + highlight_lrs: list = None, + y: str = "n_spots_sig", + color: str = "gold", + figsize: tuple = None, + highlight_color: str = "red", + max_text: int = 50, + lr_text_fp: dict = None, + ax: Axes = None, + show: bool = True, ): """Plotting the top LRs ranked by number of significant spots. @@ -175,17 +175,17 @@ def lr_summary( def lr_n_spots( - adata, - n_top: int = 100, - font_dict: dict = None, - xtick_dict: dict = None, - bar_width: float = 1, - max_text: int = 50, - non_sig_color: str = "dodgerblue", - sig_color: str = "springgreen", - figsize: tuple = (6, 4), - show_title: bool = True, - show: bool = True, + adata, + n_top: int = 100, + font_dict: dict = None, + xtick_dict: dict = None, + bar_width: float = 1, + max_text: int = 50, + non_sig_color: str = "dodgerblue", + sig_color: str = "springgreen", + figsize: tuple = (6, 4), + show_title: bool = True, + show: bool = True, ): """Bar plot showing for each LR no. of sig versus non-sig spots. @@ -257,15 +257,15 @@ def lr_n_spots( def lr_go( - adata, - n_top: int = 20, - highlight_go: list = None, - figsize=(6, 4), - rot: float = 50, - lr_text_fp: dict = None, - highlight_color: str = "yellow", - max_text: int = 50, - show: bool = True, + adata, + n_top: int = 20, + highlight_go: list = None, + figsize=(6, 4), + rot: float = 50, + lr_text_fp: dict = None, + highlight_color: str = "yellow", + max_text: int = 50, + show: bool = True, ): """Plots the results from the LR GO analysis. @@ -321,13 +321,13 @@ def lr_go( def cci_check( - adata: AnnData, - use_label: str, - figsize=(16, 10), - cell_label_size=20, - axis_text_size=18, - tick_size=14, - show=True, + adata: AnnData, + use_label: str, + figsize=(16, 10), + cell_label_size=20, + axis_text_size=18, + tick_size=14, + show=True, ): """Checks relationship between no. of significant CCI-LR interactions and cell type frequency. @@ -422,32 +422,32 @@ def cci_check( # Functions for visualisation the LR results per spot. def lr_result_plot( - adata: AnnData, - use_lr: Optional["str"] = None, - use_result: Optional["str"] = "lr_sig_scores", - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - zoom_coord: float | None = None, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - contour: bool = False, - step_size: int | None = None, - vmin: float = None, - vmax: float = None, + adata: AnnData, + use_lr: Optional["str"] = None, + use_result: Optional["str"] = "lr_sig_scores", + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + zoom_coord: float | None = None, + crop: bool | None = True, + margin: float | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, ): """Plots the per spot statistics for given LR. @@ -538,35 +538,35 @@ def lr_result_plot( # @_docs_params(het_plot=doc_lr_plot) def lr_plot( - adata: AnnData, - lr: str, - min_expr: float = 0, - sig_spots=True, - use_label: str = None, - outer_mode: str = "continuous", - l_cmap=None, - r_cmap=None, - lr_cmap=None, - inner_cmap=None, - inner_size_prop: float = 0.25, - middle_size_prop: float = 0.5, - outer_size_prop: float = 1, - pt_scale: int = 100, - title="", - show_image: bool = True, - show_arrows: bool = False, - fig: Figure = None, - ax: Axes = None, - arrow_head_width: float = 4, - arrow_width: float = 0.001, - arrow_cmap: str = None, - arrow_vmax: float = None, - sig_cci: bool = False, - lr_colors: dict = None, - figsize: tuple = (6.4, 4.8), - use_mix: bool = None, - # plotting params - **kwargs, + adata: AnnData, + lr: str, + min_expr: float = 0, + sig_spots=True, + use_label: str = None, + outer_mode: str = "continuous", + l_cmap=None, + r_cmap=None, + lr_cmap=None, + inner_cmap=None, + inner_size_prop: float = 0.25, + middle_size_prop: float = 0.5, + outer_size_prop: float = 1, + pt_scale: int = 100, + title="", + show_image: bool = True, + show_arrows: bool = False, + fig: Figure = None, + ax: Axes = None, + arrow_head_width: float = 4, + arrow_width: float = 0.001, + arrow_cmap: str = None, + arrow_vmax: float = None, + sig_cci: bool = False, + lr_colors: dict = None, + figsize: tuple = (6.4, 4.8), + use_mix: bool = None, + # plotting params + **kwargs, ) -> AnnData | None: """Creates different kinds of spatial visualisations for the LR analysis results. To see combinations of parameters refer to stLearn CCI tutorial. @@ -672,10 +672,10 @@ def lr_plot( # Making sure have run_cci first with respective labelling # if ( - show_arrows - and sig_cci - and use_label - and f"per_lr_cci_{use_label}" not in adata.uns + show_arrows + and sig_cci + and use_label + and f"per_lr_cci_{use_label}" not in adata.uns ): raise Exception( "Cannot subset arrow interactions to significant ccis " @@ -701,19 +701,16 @@ def lr_plot( "to adata.uns matching the use_mix ({use_mix}) key." ) elif ( - use_label is not None - and use_label in lr_use_labels - and ran_sig - and not lr_sig + use_label is not None and use_label in lr_use_labels and ran_sig and not lr_sig ): raise Exception( "Since use_label refers to lr stats & ran permutation testing, " "LR needs to be significant to view stats." ) elif ( - use_label is not None - and use_label not in adata.obs.keys() - and use_label not in lr_use_labels + use_label is not None + and use_label not in adata.obs.keys() + and use_label not in lr_use_labels ): raise Exception( f"use_label must be in adata.obs or " f"one of lr stats: {lr_use_labels}." @@ -905,34 +902,34 @@ def lr_plot( #### from old data structure when only test individual LRs. @_docs_params(spatial_base_plot=doc_spatial_base_plot, het_plot=doc_het_plot) def het_plot( - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - zoom_coord: float | None = None, - crop: bool | None = True, - margin: bool | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # cci_rank param - use_het: str | None = "het", - contour: bool = False, - step_size: int | None = None, - vmin: float = None, - vmax: float = None, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # cci_rank param + use_het: str | None = "het", + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, ) -> AnnData | None: """\ Allows the visualization of significant cell-cell interaction @@ -989,22 +986,22 @@ def het_plot( def ccinet_plot( - adata: AnnData, - use_label: str, - lr: str = None, - pos: dict = None, - return_pos: bool = False, - cmap: str = "default", - font_size: int = 12, - node_size_exp: int = 1, - node_size_scaler: int = 1, - min_counts: int = 0, - sig_interactions: bool = True, - fig: matplotlib.figure.Figure = None, - ax: matplotlib.axes.Axes = None, - pad=0.25, - title: str = None, - figsize: tuple = (10, 10), + adata: AnnData, + use_label: str, + lr: str = None, + pos: dict = None, + return_pos: bool = False, + cmap: str = "default", + font_size: int = 12, + node_size_exp: int = 1, + node_size_scaler: int = 1, + min_counts: int = 0, + sig_interactions: bool = True, + fig: matplotlib.figure.Figure = None, + ax: matplotlib.axes.Axes = None, + pad=0.25, + title: str = None, + figsize: tuple = (10, 10), ): """Circular celltype-celltype interaction network based on LR-CCI analysis. The size of the nodes drawn for each cell type indicates the total no. of @@ -1086,10 +1083,9 @@ def ccinet_plot( node_sizes = np.array( [ ( - ((sum(int_matrix[i, :] + int_matrix[:, i]) - int_matrix[ - i, i]) / total) - * 10000 - * node_size_scaler + ((sum(int_matrix[i, :] + int_matrix[:, i]) - int_matrix[i, i]) / total) + * 10000 + * node_size_scaler ) ** (node_size_exp) for i in node_indices @@ -1103,8 +1099,8 @@ def ccinet_plot( trans_i = np.where(all_set == edge[0][0])[0][0] receive_i = np.where(all_set == edge[0][1])[0][0] e_total = ( - sum(list(int_matrix[trans_i, :]) + list(int_matrix[:, receive_i])) - - int_matrix[trans_i, receive_i] + sum(list(int_matrix[trans_i, :]) + list(int_matrix[:, receive_i])) + - int_matrix[trans_i, receive_i] ) # so don't double count e_totals.append(e_total) edge_weights = [edge[1]["weight"] / e_totals[i] for i, edge in enumerate(edges)] @@ -1171,15 +1167,15 @@ def ccinet_plot( def cci_map( - adata: AnnData, - use_label: str, - lr: str = None, - ax: matplotlib.figure.Axes = None, - show: bool = False, - figsize: tuple = None, - cmap: str = "Spectral_r", - sig_interactions: bool = True, - title=None, + adata: AnnData, + use_label: str, + lr: str = None, + ax: matplotlib.figure.Axes = None, + show: bool = False, + figsize: tuple = None, + cmap: str = "Spectral_r", + sig_interactions: bool = True, + title=None, ): """Heatmap visualising sender->receivers of cell type interactions. @@ -1250,18 +1246,18 @@ def cci_map( def lr_cci_map( - adata: AnnData, - use_label: str, - lrs: list or np.array = None, - n_top_lrs: int = 5, - n_top_ccis: int = 15, - min_total: int = 0, - ax: matplotlib.figure.Axes = None, - figsize: tuple = (6.48, 4.8), - show: bool = False, - cmap: str = "Spectral_r", - square_scaler: int = 700, - sig_interactions: bool = True, + adata: AnnData, + use_label: str, + lrs: list or np.array = None, + n_top_lrs: int = 5, + n_top_ccis: int = 15, + min_total: int = 0, + ax: matplotlib.figure.Axes = None, + figsize: tuple = (6.48, 4.8), + show: bool = False, + cmap: str = "Spectral_r", + square_scaler: int = 700, + sig_interactions: bool = True, ): """Heatmap of interaction counts. Rows are lrs and columns are celltype->celltype interactions. @@ -1368,18 +1364,18 @@ def lr_cci_map( def lr_chord_plot( - adata: AnnData, - use_label: str, - lr: str = None, - min_ints: int = 2, - n_top_ccis: int = 10, - cmap: str = "default", - sig_interactions: bool = True, - label_size: int = 10, - label_rotation: float = 0, - title: str = None, - figsize: tuple = (8, 8), - show: bool = True, + adata: AnnData, + use_label: str, + lr: str = None, + min_ints: int = 2, + n_top_ccis: int = 10, + cmap: str = "default", + sig_interactions: bool = True, + label_size: int = 10, + label_rotation: float = 0, + title: str = None, + figsize: tuple = (8, 8), + show: bool = True, ): """Chord diagram of interactions between cell types. Note that interaction is measured as the total no. of edges connecting @@ -1487,7 +1483,7 @@ def lr_chord_plot( rotation = nodePos[i][2] # Prevent text going upside down at certain rotations if (rotation < 90 and rotation > 18 and label_rotation != 0) or ( - rotation < 120 and rotation > 90 + rotation < 120 and rotation > 90 ): label_rotation_ = -label_rotation else: @@ -1503,13 +1499,13 @@ def lr_chord_plot( def grid_plot( - adata, - use_label: str = None, - n_row: int = 10, - n_col: int = 10, - size: int = 1, - figsize=(4.5, 4.5), - show: bool = False, + adata, + use_label: str = None, + n_row: int = 10, + n_col: int = 10, + size: int = 1, + figsize=(4.5, 4.5), + show: bool = False, ): """Plots grid over the top of spatial data to show how cells will be grouped if gridded. @@ -1587,6 +1583,7 @@ def spatialcci_plot_interactive(adata: AnnData): output_notebook() show(bokeh_object.app, notebook_handle=True) + # def het_plot_interactive(adata: AnnData): # bokeh_object = BokehCciPlot(adata) # output_notebook() diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index 1b7b456c..9fca0baa 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -19,21 +19,21 @@ def lr_scatter( - data, - feature, - highlight_lrs=None, - show_text=True, - n_top=50, - color="gold", - alpha=0.5, - lr_text_fp=None, - axis_text_fp=None, - ax=None, - show=True, - max_text=100, - highlight_color="red", - figsize: tuple = None, - show_all: bool = False, + data, + feature, + highlight_lrs=None, + show_text=True, + n_top=50, + color="gold", + alpha=0.5, + lr_text_fp=None, + axis_text_fp=None, + ax=None, + show=True, + max_text=100, + highlight_color="red", + figsize: tuple = None, + show_all: bool = False, ): """General plotting of the LR features.""" highlight = highlight_lrs is not None @@ -108,28 +108,28 @@ def lr_scatter( def rank_scatter( - items, - y, - y_label: str = "", - x_label: str = "", - highlight_items=None, - show_text=True, - color="gold", - alpha=0.5, - lr_text_fp=None, - axis_text_fp=None, - ax=None, - show=True, - highlight_color="red", - rot: float = 90, - point_sizes: np.array = None, - pad=0.2, - figsize=None, - width_ratio=7.5 / 50, - height=4, - point_size_name="Sizes", - point_size_exp=2, - show_all: bool = False, + items, + y, + y_label: str = "", + x_label: str = "", + highlight_items=None, + show_text=True, + color="gold", + alpha=0.5, + lr_text_fp=None, + axis_text_fp=None, + ax=None, + show=True, + highlight_color="red", + rot: float = 90, + point_sizes: np.array = None, + pad=0.2, + figsize=None, + width_ratio=7.5 / 50, + height=4, + point_size_name="Sizes", + point_size_exp=2, + show_all: bool = False, ): """General plotting function for showing ranked list of items.""" ranks = np.array(list(range(len(items)))) @@ -154,7 +154,7 @@ def rank_scatter( y, alpha=alpha, c=color, - s=None if point_sizes is None else point_sizes ** point_size_exp, + s=None if point_sizes is None else point_sizes**point_size_exp, edgecolors="none", ) y_min, y_max = ax.get_ylim() @@ -167,12 +167,12 @@ def rank_scatter( starts = [label.find("{") for label in labels] ends = [label.find("}") + 1 for label in labels] sizes = [ - float(label[(starts[i] + 1): (ends[i] - 1)]) + float(label[(starts[i] + 1) : (ends[i] - 1)]) for i, label in enumerate(labels) ] counts = [int(size ** (1 / point_size_exp)) for size in sizes] labels2 = [ - label.replace(label[(starts[i]): (ends[i])], "{" + str(counts[i]) + "}") + label.replace(label[(starts[i]) : (ends[i])], "{" + str(counts[i]) + "}") for i, label in enumerate(labels) ] ax.legend( @@ -220,19 +220,19 @@ def rank_scatter( def add_arrows( - adata: AnnData, - l_expr: np.array, - r_expr: np.array, - min_expr: float, - sig_bool: np.array, - fig, - ax: Axes, - use_label: str, - int_df: pd.DataFrame, - head_width=4, - width=0.001, - arrow_cmap=None, - arrow_vmax=None, + adata: AnnData, + l_expr: np.array, + r_expr: np.array, + min_expr: float, + sig_bool: np.array, + fig, + ax: Axes, + use_label: str, + int_df: pd.DataFrame, + head_width=4, + width=0.001, + arrow_cmap=None, + arrow_vmax=None, ): """ Adds arrows to the current plot for significant spots to neighbours \ which is interacting with. @@ -368,15 +368,15 @@ def add_arrows( def add_arrows_by_edges( - ax, - adata, - edges, - scale_factor, - head_width, - width, - forward=True, - edge_colors=None, - axc=None, + ax, + adata, + edges, + scale_factor, + head_width, + width, + forward=True, + edge_colors=None, + axc=None, ): """Adds the arrows using an edge list.""" for i, edge in enumerate(edges): @@ -499,11 +499,11 @@ def polar2xy(r, theta): def hex2rgb(c): - return tuple(int(c[i: i + 2], 16) / 256.0 for i in (1, 3, 5)) + return tuple(int(c[i : i + 2], 16) / 256.0 for i in (1, 3, 5)) def IdeogramArc( - start=0, end=60, radius=1.0, width=0.2, ax=None, color=(1, 0, 0), curve_steps=1 + start=0, end=60, radius=1.0, width=0.2, ax=None, color=(1, 0, 0), curve_steps=1 ): # start, end should be in [0, 360) if start > end: @@ -543,22 +543,22 @@ def IdeogramArc( for i in range(1, curve_steps + 1) ] verts_inner = ( - verts_inner_start - + verts_inner_curve - + [polar2xy(inner, start), polar2xy(radius, start)] + verts_inner_start + + verts_inner_curve + + [polar2xy(inner, start), polar2xy(radius, start)] ) verts = verts_upper + verts_inner codes = ( - [Path.MOVETO] - + [Path.CURVE4] * curve_steps * 2 - + [Path.CURVE4, Path.LINETO] - + [Path.CURVE4] * curve_steps * 2 - + [ - Path.CURVE4, - Path.CLOSEPOLY, - ] + [Path.MOVETO] + + [Path.CURVE4] * curve_steps * 2 + + [Path.CURVE4, Path.LINETO] + + [Path.CURVE4] * curve_steps * 2 + + [ + Path.CURVE4, + Path.CLOSEPOLY, + ] ) if ax is None: @@ -572,14 +572,14 @@ def IdeogramArc( def ChordArc( - start1=0, - end1=60, - start2=180, - end2=240, - radius=1.0, - chordwidth=0.7, - ax=None, - color=(1, 0, 0), + start1=0, + end1=60, + start2=180, + end2=240, + radius=1.0, + chordwidth=0.7, + ax=None, + color=(1, 0, 0), ): # start, end should be in [0, 360) if start1 > end1: diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index a435cbf3..14efdcc2 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -8,7 +8,7 @@ import warnings from typing import ( # Special Optional, # Classes - ) +) import matplotlib import matplotlib.pyplot as plt @@ -25,31 +25,31 @@ class SpatialBasePlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - zoom_coord: float | None = None, - crop: bool | None = True, - margin: bool | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 0.7, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - **kwds, + self, + # plotting param + adata: AnnData, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 0.7, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + **kwds, ): super().__init__( adata, @@ -73,7 +73,7 @@ def __init__( if use_label is not None: assert ( - use_label in self.adata[0].obs.columns + use_label in self.adata[0].obs.columns ), "Please choose the right label in `adata.obs.columns`!" self.use_label = use_label @@ -103,8 +103,8 @@ def __init__( stlearn_cmap = ["jana_40", "default"] cmap_available = plt.colormaps() + scanpy_cmap + stlearn_cmap error_msg = ( - "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" - "one of these: " + str(cmap_available) + "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" + "one of these: " + str(cmap_available) ) if cmap is str: assert cmap in cmap_available, error_msg @@ -139,7 +139,7 @@ def create_query(list_cl, use_label): if self.list_clusters is not None: # IF not all clusters specified, subset, otherwise just copy. if len(self.list_clusters) != len( - self.adata[0].obs[self.use_label].cat.categories + self.adata[0].obs[self.use_label].cat.categories ): self.query_adata = self.query_adata[ self.query_adata.obs.query( @@ -223,41 +223,42 @@ def _save_output(self): # # ################################################################ + class GenePlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - crop: bool | None = True, - zoom_coord: float | None = None, - margin: bool | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # gene plot param - gene_symbols: str | list = None, - threshold: float | None = None, - method: str = "CumSum", - contour: bool = False, - step_size: int | None = None, - vmin: float = None, - vmax: float = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # gene plot param + gene_symbols: str | list = None, + threshold: float | None = None, + method: str = "CumSum", + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, + **kwargs, ): super().__init__( adata=adata, @@ -437,38 +438,38 @@ def _add_threshold(self, gene_values, threshold): class FeaturePlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - crop: bool | None = True, - zoom_coord: float | None = None, - margin: bool | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # gene plot param - feature: str = None, - threshold: float | None = None, - contour: bool = False, - step_size: int | None = None, - vmin: float = None, - vmax: float = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # gene plot param + feature: str = None, + threshold: float | None = None, + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, + **kwargs, ): super().__init__( adata=adata, @@ -525,7 +526,7 @@ def _get_feature_values(self): self.feature + " is not in data.obs, please try another feature" ) elif not isinstance( - self.query_adata.obs[self.feature].values[0], numbers.Number + self.query_adata.obs[self.feature].values[0], numbers.Number ): raise ValueError( self.feature @@ -601,44 +602,44 @@ def _add_threshold(self, feature_values, threshold): # Cluster plot class class ClusterPlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "default", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - zoom_coord: float | None = None, - margin: bool | None = 100, - size: float | None = 5, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - fname: str | None = None, - dpi: int | None = 120, - # cluster plot param - show_subcluster: bool | None = False, - show_cluster_labels: bool | None = False, - show_trajectories: bool | None = False, - reverse: bool | None = False, - show_node: bool | None = False, - threshold_spots: int | None = 5, - text_box_size: float | None = 5, - color_bar_size: float | None = 10, - bbox_to_anchor: tuple[float, float] | None = (1, 1), - # trajectory - trajectory_node_size: int | None = 10, - trajectory_alpha: float | None = 1.0, - trajectory_width: float | None = 2.5, - trajectory_edge_color: str | None = "#f4efd3", - trajectory_arrowsize: int | None = 17, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "default", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 5, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + fname: str | None = None, + dpi: int | None = 120, + # cluster plot param + show_subcluster: bool | None = False, + show_cluster_labels: bool | None = False, + show_trajectories: bool | None = False, + reverse: bool | None = False, + show_node: bool | None = False, + threshold_spots: int | None = 5, + text_box_size: float | None = 5, + color_bar_size: float | None = 10, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + # trajectory + trajectory_node_size: int | None = 10, + trajectory_alpha: float | None = 1.0, + trajectory_width: float | None = 2.5, + trajectory_edge_color: str | None = "#f4efd3", + trajectory_arrowsize: int | None = 17, ): super().__init__( adata=adata, @@ -763,7 +764,7 @@ def _add_cluster_labels(self): label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ].index + ].index ) subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), label_index) @@ -806,7 +807,7 @@ def _add_sub_clusters(self): label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ].index + ].index ) subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), label_index) @@ -816,18 +817,18 @@ def _add_sub_clusters(self): imgrow_new = subset_spatial[:, 1] * self.scale_factor if ( - len( - self.query_adata.obs[ - self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"].unique() - ) - < 2 + len( + self.query_adata.obs[ + self.query_adata.obs[self.use_label] == str(label) + ]["sub_cluster_labels"].unique() + ) + < 2 ): centroids = [centroidpython(imgcol_new, imgrow_new)] classes = np.array( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"].unique() + ]["sub_cluster_labels"].unique() ) else: @@ -838,7 +839,7 @@ def _add_sub_clusters(self): np.column_stack((imgcol_new, imgrow_new)), self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"], + ]["sub_cluster_labels"], ) centroids = clf.centroids_ @@ -846,12 +847,12 @@ def _add_sub_clusters(self): for j, label in enumerate(classes): if ( - len( - self.query_adata.obs[ - self.query_adata.obs["sub_cluster_labels"] == label - ] - ) - > self.threshold_spots + len( + self.query_adata.obs[ + self.query_adata.obs["sub_cluster_labels"] == label + ] + ) + > self.threshold_spots ): if centroids[j][0] < 1500: x = -100 @@ -953,34 +954,34 @@ def _add_trajectories(self): class SubClusterPlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "jet", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - zoom_coord: float | None = None, - margin: bool | None = 100, - size: float | None = 5, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - fname: str | None = None, - dpi: int | None = 120, - # subcluster plot param - cluster: int | None = 0, - threshold_spots: int | None = 5, - text_box_size: float | None = 5, - bbox_to_anchor: tuple[float, float] | None = (1, 1), - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "jet", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 5, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + fname: str | None = None, + dpi: int | None = 120, + # subcluster plot param + cluster: int | None = 0, + threshold_spots: int | None = 5, + text_box_size: float | None = 5, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + **kwargs, ): super().__init__( adata=adata, @@ -1107,36 +1108,36 @@ def _add_subclusters_label(self, subset): class CciPlot(GenePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - zoom_coord: float | None = None, - margin: bool | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # cci_rank param - use_het: str | None = "het", - contour: bool = False, - step_size: int | None = None, - vmin: float = None, - vmax: float = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # cci_rank param + use_het: str | None = "het", + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, + **kwargs, ): super().__init__( adata=adata, @@ -1176,36 +1177,36 @@ def _get_gene_expression(self): class LrResultPlot(GenePlot): def __init__( - self, - adata: AnnData, - use_lr: Optional["str"] = None, - use_result: Optional["str"] = "lr_sig_scores", - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - zoom_coord: float | None = None, - margin: bool | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # cci_rank param - contour: bool = False, - step_size: int | None = None, - vmin: float = None, - vmax: float = None, - **kwargs, + self, + adata: AnnData, + use_lr: Optional["str"] = None, + use_result: Optional["str"] = "lr_sig_scores", + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + zoom_coord: float | None = None, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + # cci_rank param + contour: bool = False, + step_size: int | None = None, + vmin: float = None, + vmax: float = None, + **kwargs, ): # Making sure cci_rank has been run first # if "lr_summary" not in adata.uns: diff --git a/stlearn/plotting/classes_bokeh.py b/stlearn/plotting/classes_bokeh.py index 91f678a3..8b5d6fd0 100644 --- a/stlearn/plotting/classes_bokeh.py +++ b/stlearn/plotting/classes_bokeh.py @@ -1,4 +1,3 @@ - from collections import OrderedDict import numpy as np @@ -50,9 +49,9 @@ class BokehGenePlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__( adata, @@ -322,9 +321,9 @@ def create_violin(self, adata, gene_symbol, use_label): class BokehClusterPlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__(adata) @@ -471,8 +470,8 @@ def __init__( if "rank_genes_groups" in self.adata[0].uns: if ( - self.use_label.value - == self.adata[0].uns["rank_genes_groups"]["params"]["groupby"] + self.use_label.value + == self.adata[0].uns["rank_genes_groups"]["params"]["groupby"] ): self.layout = column(row(self.inputs, self.make_fig()), self.add_dea()) else: @@ -519,8 +518,8 @@ def update_data(self, attrname, old, new): if "rank_genes_groups" in self.adata[0].uns: if ( - self.use_label.value - == self.adata[0].uns["rank_genes_groups"]["params"]["groupby"] + self.use_label.value + == self.adata[0].uns["rank_genes_groups"]["params"]["groupby"] ): self.layout.children[0].children[1] = self.make_fig() self.layout.children[1] = self.add_dea() @@ -764,9 +763,9 @@ def create_dea(self, adata): class BokehLRPlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__( adata, @@ -942,9 +941,9 @@ def _get_lr(self, lr): class BokehSpatialCciPlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__( adata, @@ -1213,6 +1212,7 @@ def _add_edges(fig, adata, edges, arrow_size, forward=True, scale_factor=1): def update_list(self, attrname, old, name): # Initialize the color from stlearn.plotting.cluster_plot import cluster_plot + selected = self.annot_select.value.strip("raw_") cluster_plot(self.adata[0], use_label=selected, show_plot=False) self.list_cluster.labels = list(self.adata[0].obs[selected].cat.categories) @@ -1223,9 +1223,9 @@ def update_list(self, attrname, old, name): class Annotate(Spatial): def __init__( - self, - # plotting param - adata: AnnData, + self, + # plotting param + adata: AnnData, ): super().__init__(adata) # Open image, and make sure it's RGB*A* diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index 84254e82..c5d1b08e 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -1,6 +1,6 @@ from typing import ( Optional, # Special - ) +) import matplotlib from anndata import AnnData @@ -15,43 +15,43 @@ @_docs_params(spatial_base_plot=doc_spatial_base_plot, cluster_plot=doc_cluster_plot) def cluster_plot( - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "default", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - zoom_coord: float | None = None, - crop: bool | None = True, - margin: bool | None = 100, - size: float | None = 5, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - fname: str | None = None, - dpi: int | None = 120, - # cluster plot param - show_subcluster: bool | None = False, - show_cluster_labels: bool | None = False, - show_trajectories: bool | None = False, - reverse: bool | None = False, - show_node: bool | None = False, - threshold_spots: int | None = 5, - text_box_size: float | None = 5, - color_bar_size: float | None = 10, - bbox_to_anchor: tuple[float, float] | None = (1, 1), - # trajectory - trajectory_node_size: int | None = 10, - trajectory_alpha: float | None = 1.0, - trajectory_width: float | None = 2.5, - trajectory_edge_color: str | None = "#f4efd3", - trajectory_arrowsize: int | None = 17, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "default", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 5, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + fname: str | None = None, + dpi: int | None = 120, + # cluster plot param + show_subcluster: bool | None = False, + show_cluster_labels: bool | None = False, + show_trajectories: bool | None = False, + reverse: bool | None = False, + show_node: bool | None = False, + threshold_spots: int | None = 5, + text_box_size: float | None = 5, + color_bar_size: float | None = 10, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + # trajectory + trajectory_node_size: int | None = 10, + trajectory_alpha: float | None = 1.0, + trajectory_width: float | None = 2.5, + trajectory_edge_color: str | None = "#f4efd3", + trajectory_arrowsize: int | None = 17, ) -> AnnData | None: """\ Allows the visualization of a cluster results as the discretes values @@ -114,7 +114,7 @@ def cluster_plot( def cluster_plot_interactive( - adata: AnnData, + adata: AnnData, ): bokeh_object = BokehClusterPlot(adata) output_notebook() diff --git a/stlearn/plotting/deconvolution_plot.py b/stlearn/plotting/deconvolution_plot.py index 632dfadb..f41a9f8d 100644 --- a/stlearn/plotting/deconvolution_plot.py +++ b/stlearn/plotting/deconvolution_plot.py @@ -1,4 +1,3 @@ - import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np @@ -6,30 +5,30 @@ def deconvolution_plot( - adata: AnnData, - library_id: str = None, - use_label: str = "louvain", - cluster: [int, str] = None, - celltype: str = None, - celltype_threshold: float = 0, - data_alpha: float = 1.0, - threshold: float = 0.0, - cmap: str = "tab20", - colors: list = None, # The colors to use for each label... - tissue_alpha: float = 1.0, - title: str = None, - spot_size: float | int = 10, - show_axis: bool = False, - show_legend: bool = True, - show_donut: bool = True, - cropped: bool = True, - margin: int = 100, - name: str = None, - dpi: int = 150, - output: str = None, - copy: bool = False, - figsize: tuple = (6.4, 4.8), - show=True, + adata: AnnData, + library_id: str = None, + use_label: str = "louvain", + cluster: [int, str] = None, + celltype: str = None, + celltype_threshold: float = 0, + data_alpha: float = 1.0, + threshold: float = 0.0, + cmap: str = "tab20", + colors: list = None, # The colors to use for each label... + tissue_alpha: float = 1.0, + title: str = None, + spot_size: float | int = 10, + show_axis: bool = False, + show_legend: bool = True, + show_donut: bool = True, + cropped: bool = True, + margin: int = 100, + name: str = None, + dpi: int = 150, + output: str = None, + copy: bool = False, + figsize: tuple = (6.4, 4.8), + show=True, ) -> AnnData | None: """\ Clustering plot for sptial transcriptomics data. Also, it has a function to diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index 77b0878c..1172cbc2 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -4,7 +4,7 @@ from typing import ( Optional, # Special - ) +) import matplotlib from anndata import AnnData @@ -14,34 +14,34 @@ # @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) def feat_plot( - adata: AnnData, - feature: str = None, - threshold: float | None = None, - contour: bool = False, - step_size: int | None = None, - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - zoom_coord: float | None = None, - crop: bool | None = True, - margin: bool | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 0.7, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - vmin: float | None = None, - vmax: float | None = None, + adata: AnnData, + feature: str = None, + threshold: float | None = None, + contour: bool = False, + step_size: int | None = None, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 0.7, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + vmin: float | None = None, + vmax: float | None = None, ) -> AnnData | None: """\ Allows the visualization of a continuous features stored in adata.obs diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index 860c0b0a..d4ebcdff 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -1,6 +1,6 @@ from typing import ( # Special Optional, # Classes - ) +) import matplotlib from anndata import AnnData @@ -15,35 +15,35 @@ @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) def gene_plot( - adata: AnnData, - gene_symbols: str | list = None, - threshold: float | None = None, - method: str = "CumSum", - contour: bool = False, - step_size: int | None = None, - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - zoom_coord: float | None = None, - crop: bool | None = True, - margin: bool | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 0.7, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - vmin: float | None = None, - vmax: float | None = None, + adata: AnnData, + gene_symbols: str | list = None, + threshold: float | None = None, + method: str = "CumSum", + contour: bool = False, + step_size: int | None = None, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + zoom_coord: float | None = None, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 0.7, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + vmin: float | None = None, + vmax: float | None = None, ) -> AnnData | None: """\ Allows the visualization of a single gene or multiple genes as the values diff --git a/stlearn/plotting/mask_plot.py b/stlearn/plotting/mask_plot.py index 6224b8c2..e3e13ed4 100644 --- a/stlearn/plotting/mask_plot.py +++ b/stlearn/plotting/mask_plot.py @@ -1,24 +1,23 @@ - import matplotlib from anndata import AnnData from matplotlib import pyplot as plt def plot_mask( - adata: AnnData, - library_id: str = None, - show_spot: bool = True, - spot_alpha: float = 1.0, - cmap: str = "vega_20_scanpy", - tissue_alpha: float = 1.0, - mask_alpha: float = 0.5, - spot_size: float | int = 6.5, - show_legend: bool = True, - name: str = "mask_plot", - dpi: int = 150, - output: str = None, - show_axis: bool = False, - show_plot: bool = True, + adata: AnnData, + library_id: str = None, + show_spot: bool = True, + spot_alpha: float = 1.0, + cmap: str = "vega_20_scanpy", + tissue_alpha: float = 1.0, + mask_alpha: float = 0.5, + spot_size: float | int = 6.5, + show_legend: bool = True, + name: str = "mask_plot", + dpi: int = 150, + output: str = None, + show_axis: bool = False, + show_plot: bool = True, ) -> AnnData | None: """\ mask plot for sptial transcriptomics data. diff --git a/stlearn/plotting/non_spatial_plot.py b/stlearn/plotting/non_spatial_plot.py index e196b8f2..600f7897 100644 --- a/stlearn/plotting/non_spatial_plot.py +++ b/stlearn/plotting/non_spatial_plot.py @@ -1,12 +1,11 @@ - # from .utils import get_img_from_fig, checkType import scanpy from anndata import AnnData def non_spatial_plot( - adata: AnnData, - use_label: str = "louvain", + adata: AnnData, + use_label: str = "louvain", ) -> AnnData | None: """\ A wrap function to plot all the non-spatial plot from scanpy. diff --git a/stlearn/plotting/stack_3d_plot.py b/stlearn/plotting/stack_3d_plot.py index 17ee34ff..e13bba29 100644 --- a/stlearn/plotting/stack_3d_plot.py +++ b/stlearn/plotting/stack_3d_plot.py @@ -1,17 +1,16 @@ - import pandas as pd from anndata import AnnData def stack_3d_plot( - adata: AnnData, - slides, - height, - width, - cmap="viridis", - slide_col="sample_id", - use_label=None, - gene_symbol=None, + adata: AnnData, + slides, + height, + width, + cmap="viridis", + slide_col="sample_id", + use_label=None, + gene_symbol=None, ) -> AnnData | None: """\ Clustering plot for spatial transcriptomics data. Also, it has a function to @@ -45,7 +44,7 @@ def stack_3d_plot( raise ModuleNotFoundError("Please install plotly by `pip install plotly`") assert ( - slide_col in adata.obs.columns + slide_col in adata.obs.columns ), "Please provide the right column for slide_id!" list_df = [] diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/plotting/subcluster_plot.py index bdca7f54..af603e13 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/plotting/subcluster_plot.py @@ -1,6 +1,6 @@ from typing import ( Optional, # Special - ) +) from anndata import AnnData @@ -13,30 +13,30 @@ spatial_base_plot=doc_spatial_base_plot, subcluster_plot=doc_subcluster_plot ) def subcluster_plot( - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "jet", - use_label: str | None = None, - list_clusters: list | None = None, - ax: _AxesSubplot | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - margin: bool | None = 100, - size: float | None = 5, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - fname: str | None = None, - dpi: int | None = 120, - # subcluster plot param - cluster: int | None = 0, - threshold_spots: int | None = 5, - text_box_size: float | None = 5, - bbox_to_anchor: tuple[float, float] | None = (1, 1), + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "jet", + use_label: str | None = None, + list_clusters: list | None = None, + ax: _AxesSubplot | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + crop: bool | None = True, + margin: bool | None = 100, + size: float | None = 5, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 1.0, + fname: str | None = None, + dpi: int | None = 120, + # subcluster plot param + cluster: int | None = 0, + threshold_spots: int | None = 5, + text_box_size: float | None = 5, + bbox_to_anchor: tuple[float, float] | None = (1, 1), ) -> AnnData | None: """\ Allows the visualization of a subclustering results as the discretes values @@ -59,7 +59,7 @@ def subcluster_plot( assert use_label is not None, "Please select `use_label` parameter" assert ( - use_label in adata.obs.columns + use_label in adata.obs.columns ), "Please run `stlearn.spatial.cluster.localization` function!" SubClusterPlot( diff --git a/stlearn/plotting/trajectory/DE_transition_plot.py b/stlearn/plotting/trajectory/DE_transition_plot.py index c70bfd6e..55275f6b 100644 --- a/stlearn/plotting/trajectory/DE_transition_plot.py +++ b/stlearn/plotting/trajectory/DE_transition_plot.py @@ -5,12 +5,12 @@ def DE_transition_plot( - adata: AnnData, - top_genes: int = 10, - font_size: int = 6, - name: str = None, - dpi: int = 150, - output: str = None, + adata: AnnData, + top_genes: int = 10, + font_size: int = 6, + name: str = None, + dpi: int = 150, + output: str = None, ) -> AnnData | None: """\ Differential expression between transition markers. diff --git a/stlearn/plotting/trajectory/check_trajectory.py b/stlearn/plotting/trajectory/check_trajectory.py index 5cab6744..d9e84f12 100644 --- a/stlearn/plotting/trajectory/check_trajectory.py +++ b/stlearn/plotting/trajectory/check_trajectory.py @@ -1,4 +1,3 @@ - import matplotlib.pyplot as plt import numpy as np import scanpy as sc @@ -6,30 +5,30 @@ def check_trajectory( - adata: AnnData, - library_id: str = None, - use_label: str = "louvain", - basis: str = "umap", - pseudotime_key: str = "dpt_pseudotime", - trajectory: list = None, - figsize=(10, 4), - size_umap: int = 50, - size_spatial: int = 1.5, - img_key: str = "hires", + adata: AnnData, + library_id: str = None, + use_label: str = "louvain", + basis: str = "umap", + pseudotime_key: str = "dpt_pseudotime", + trajectory: list = None, + figsize=(10, 4), + size_umap: int = 50, + size_spatial: int = 1.5, + img_key: str = "hires", ) -> AnnData | None: trajectory = np.array(trajectory).astype(int) assert ( - trajectory in adata.uns["available_paths"].values() + trajectory in adata.uns["available_paths"].values() ), "Please choose the right path!" trajectory = trajectory.astype(str) assert ( - pseudotime_key in adata.obs.columns + pseudotime_key in adata.obs.columns ), "Please run the pseudotime or choose the right one!" assert ( - use_label in adata.obs.columns + use_label in adata.obs.columns ), "Please run the cluster or choose the right label!" assert basis in adata.obsm, ( - "Please run the " + basis + "before you check the trajectory!" + "Please run the " + basis + "before you check the trajectory!" ) if library_id is None: library_id = list(adata.uns["spatial"].keys())[0] @@ -66,9 +65,7 @@ def check_trajectory( show=False, ) - ax2.imshow( - adata.uns["spatial"][library_id]["images"][img_key], alpha=0, zorder=-1 - ) + ax2.imshow(adata.uns["spatial"][library_id]["images"][img_key], alpha=0, zorder=-1) plt.show() diff --git a/stlearn/plotting/trajectory/local_plot.py b/stlearn/plotting/trajectory/local_plot.py index f1c0c7a6..de9a9bca 100644 --- a/stlearn/plotting/trajectory/local_plot.py +++ b/stlearn/plotting/trajectory/local_plot.py @@ -1,4 +1,3 @@ - import matplotlib.pyplot as plt import numpy as np from anndata import AnnData @@ -7,22 +6,22 @@ def local_plot( - adata: AnnData, - use_label: str = "louvain", - use_cluster: int = None, - reverse: bool = False, - cluster: int = 0, - data_alpha: float = 1.0, - arrow_alpha: float = 1.0, - branch_alpha: float = 0.2, - spot_size: float | int = 1, - show_color_bar: bool = True, - show_axis: bool = False, - show_plot: bool = True, - name: str = None, - dpi: int = 150, - output: str = None, - copy: bool = False, + adata: AnnData, + use_label: str = "louvain", + use_cluster: int = None, + reverse: bool = False, + cluster: int = 0, + data_alpha: float = 1.0, + arrow_alpha: float = 1.0, + branch_alpha: float = 0.2, + spot_size: float | int = 1, + show_color_bar: bool = True, + show_axis: bool = False, + show_plot: bool = True, + name: str = None, + dpi: int = 150, + output: str = None, + copy: bool = False, ) -> AnnData | None: """\ Local spatial trajectory inference plot. @@ -76,8 +75,8 @@ def local_plot( order = 0 for i in ref_cluster.obs["sub_cluster_labels"].unique(): if ( - len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) - > adata.uns["threshold_spots"] + len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) + > adata.uns["threshold_spots"] ): classes_.append(i) centroid_dict = adata.uns["centroid_dict"] diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index 6a92528f..bbe1b098 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -1,4 +1,3 @@ - import matplotlib import networkx as nx import numpy as np @@ -9,31 +8,31 @@ def pseudotime_plot( - adata: AnnData, - library_id: str = None, - use_label: str = "louvain", - pseudotime_key: str = "pseudotime_key", - list_clusters: str | list = None, - cell_alpha: float = 1.0, - image_alpha: float = 1.0, - edge_alpha: float = 0.8, - node_alpha: float = 1.0, - spot_size: float | int = 6.5, - node_size: float = 5, - show_color_bar: bool = True, - show_axis: bool = False, - show_graph: bool = True, - show_trajectories: bool = False, - reverse: bool = False, - show_node: bool = True, - show_plot: bool = True, - cropped: bool = True, - margin: int = 100, - dpi: int = 150, - output: str = None, - name: str = None, - copy: bool = False, - ax=None, + adata: AnnData, + library_id: str = None, + use_label: str = "louvain", + pseudotime_key: str = "pseudotime_key", + list_clusters: str | list = None, + cell_alpha: float = 1.0, + image_alpha: float = 1.0, + edge_alpha: float = 0.8, + node_alpha: float = 1.0, + spot_size: float | int = 6.5, + node_size: float = 5, + show_color_bar: bool = True, + show_axis: bool = False, + show_graph: bool = True, + show_trajectories: bool = False, + reverse: bool = False, + show_node: bool = True, + show_plot: bool = True, + cropped: bool = True, + margin: int = 100, + dpi: int = 150, + output: str = None, + name: str = None, + copy: bool = False, + ax=None, ) -> AnnData | None: """\ Global trajectory inference plot (Only DPT). diff --git a/stlearn/plotting/trajectory/transition_markers_plot.py b/stlearn/plotting/trajectory/transition_markers_plot.py index 267a6e64..6d2bf260 100644 --- a/stlearn/plotting/trajectory/transition_markers_plot.py +++ b/stlearn/plotting/trajectory/transition_markers_plot.py @@ -5,12 +5,12 @@ def transition_markers_plot( - adata: AnnData, - top_genes: int = 10, - trajectory: str = None, - dpi: int = 150, - output: str = None, - name: str = None, + adata: AnnData, + top_genes: int = 10, + trajectory: str = None, + dpi: int = 150, + output: str = None, + name: str = None, ) -> AnnData | None: """\ Plot transition marker. diff --git a/stlearn/plotting/trajectory/tree_plot.py b/stlearn/plotting/trajectory/tree_plot.py index c7999321..71992608 100644 --- a/stlearn/plotting/trajectory/tree_plot.py +++ b/stlearn/plotting/trajectory/tree_plot.py @@ -9,22 +9,22 @@ def tree_plot( - adata: AnnData, - library_id: str = None, - figsize: float | int = (10, 4), - data_alpha: float = 1.0, - use_label: str = "louvain", - spot_size: float | int = 50, - fontsize: int = 6, - piesize: float = 0.15, - zoom: float = 0.1, - name: str = None, - output: str = None, - dpi: int = 180, - show_all: bool = False, - show_plot: bool = True, - ncols: int = 4, - copy: bool = False, + adata: AnnData, + library_id: str = None, + figsize: float | int = (10, 4), + data_alpha: float = 1.0, + use_label: str = "louvain", + spot_size: float | int = 50, + fontsize: int = 6, + piesize: float = 0.15, + zoom: float = 0.1, + name: str = None, + output: str = None, + dpi: int = 180, + show_all: bool = False, + show_plot: bool = True, + ncols: int = 4, + copy: bool = False, ) -> AnnData | None: """\ Hierarchical tree plot represent for the global spatial trajectory inference. @@ -144,8 +144,7 @@ def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5 root = random.choice(list(G.nodes)) def _hierarchy_pos( - G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, - parent=None + G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, parent=None ): """ see hierarchy_pos docstring for most arguments diff --git a/stlearn/plotting/trajectory/tree_plot_simple.py b/stlearn/plotting/trajectory/tree_plot_simple.py index 84c0882f..92e0cb07 100644 --- a/stlearn/plotting/trajectory/tree_plot_simple.py +++ b/stlearn/plotting/trajectory/tree_plot_simple.py @@ -9,22 +9,22 @@ def tree_plot_simple( - adata: AnnData, - library_id: str = None, - figsize: float | int = (10, 4), - data_alpha: float = 1.0, - use_label: str = "louvain", - spot_size: float | int = 50, - fontsize: int = 6, - piesize: float = 0.15, - zoom: float = 0.1, - name: str = None, - output: str = None, - dpi: int = 180, - show_all: bool = False, - show_plot: bool = True, - ncols: int = 4, - copy: bool = False, + adata: AnnData, + library_id: str = None, + figsize: float | int = (10, 4), + data_alpha: float = 1.0, + use_label: str = "louvain", + spot_size: float | int = 50, + fontsize: int = 6, + piesize: float = 0.15, + zoom: float = 0.1, + name: str = None, + output: str = None, + dpi: int = 180, + show_all: bool = False, + show_plot: bool = True, + ncols: int = 4, + copy: bool = False, ) -> AnnData | None: """\ Hierarchical tree plot represent for the global spatial trajectory inference. @@ -144,8 +144,7 @@ def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5 root = random.choice(list(G.nodes)) def _hierarchy_pos( - G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, - parent=None + G, root, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5, pos=None, parent=None ): """ see hierarchy_pos docstring for most arguments diff --git a/stlearn/plotting/utils.py b/stlearn/plotting/utils.py index 9c5eecf0..6304d740 100644 --- a/stlearn/plotting/utils.py +++ b/stlearn/plotting/utils.py @@ -33,10 +33,10 @@ def centroidpython(x, y): def get_cluster(search, dictionary): for ( - cl, - sub, + cl, + sub, ) in ( - dictionary.items() + dictionary.items() ): # for name, age in dictionary.iteritems(): (for Python 2.x) if search in sub: return cl @@ -91,8 +91,8 @@ def check_cmap(cmap): stlearn_cmap = ["jana_40", "default"] cmap_available = plt.colormaps() + scanpy_cmap + stlearn_cmap error_msg = ( - "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" - "one of these: " + str(cmap_available) + "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" + "one of these: " + str(cmap_available) ) if cmap is str: assert cmap in cmap_available, error_msg diff --git a/stlearn/preprocessing/filter_genes.py b/stlearn/preprocessing/filter_genes.py index 260773bc..42cc3d24 100644 --- a/stlearn/preprocessing/filter_genes.py +++ b/stlearn/preprocessing/filter_genes.py @@ -1,16 +1,15 @@ - import numpy as np import scanpy from anndata import AnnData def filter_genes( - adata: AnnData, - min_counts: int | None = None, - min_cells: int | None = None, - max_counts: int | None = None, - max_cells: int | None = None, - inplace: bool = True, + adata: AnnData, + min_counts: int | None = None, + min_cells: int | None = None, + max_counts: int | None = None, + max_cells: int | None = None, + inplace: bool = True, ) -> AnnData | None | tuple[np.ndarray, np.ndarray]: """\ Wrap function scanpy.pp.filter_genes diff --git a/stlearn/preprocessing/graph.py b/stlearn/preprocessing/graph.py index 6d6334dd..4330abbd 100644 --- a/stlearn/preprocessing/graph.py +++ b/stlearn/preprocessing/graph.py @@ -38,16 +38,16 @@ def neighbors( - adata: AnnData, - n_neighbors: int = 15, - n_pcs: int | None = None, - use_rep: str | None = None, - knn: bool = True, - random_state: int | RandomState | None = 0, - method: _Method | None = "umap", - metric: _Metric | _MetricFn = "euclidean", - metric_kwds: Mapping[str, Any] = MappingProxyType({}), - copy: bool = False, + adata: AnnData, + n_neighbors: int = 15, + n_pcs: int | None = None, + use_rep: str | None = None, + knn: bool = True, + random_state: int | RandomState | None = 0, + method: _Method | None = "umap", + metric: _Metric | _MetricFn = "euclidean", + metric_kwds: Mapping[str, Any] = MappingProxyType({}), + copy: bool = False, ) -> AnnData | None: """\ Compute a neighborhood graph of observations [McInnes18]_. diff --git a/stlearn/preprocessing/log_scale.py b/stlearn/preprocessing/log_scale.py index 4fb216d8..9ebb63e0 100644 --- a/stlearn/preprocessing/log_scale.py +++ b/stlearn/preprocessing/log_scale.py @@ -1,4 +1,3 @@ - import numpy as np import scanpy from anndata import AnnData @@ -6,11 +5,11 @@ def log1p( - adata: AnnData | np.ndarray | spmatrix, - copy: bool = False, - chunked: bool = False, - chunk_size: int | None = None, - base: float | None = None, + adata: AnnData | np.ndarray | spmatrix, + copy: bool = False, + chunked: bool = False, + chunk_size: int | None = None, + base: float | None = None, ) -> AnnData | None: """\ Wrap function of scanpy.pp.log1p @@ -45,10 +44,10 @@ def log1p( def scale( - adata: AnnData | np.ndarray | spmatrix, - zero_center: bool = True, - max_value: float | None = None, - copy: bool = False, + adata: AnnData | np.ndarray | spmatrix, + zero_center: bool = True, + max_value: float | None = None, + copy: bool = False, ) -> AnnData | None: """\ Wrap function of scanpy.pp.scale diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index 11fd6528..f604e4fe 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -8,14 +8,14 @@ def normalize_total( - adata: AnnData, - target_sum: float | None = None, - exclude_highly_expressed: bool = False, - max_fraction: float = 0.05, - key_added: str | None = None, - layers: Literal["all"] | Iterable[str] = None, - layer_norm: str | None = None, - inplace: bool = True, + adata: AnnData, + target_sum: float | None = None, + exclude_highly_expressed: bool = False, + max_fraction: float = 0.05, + key_added: str | None = None, + layers: Literal["all"] | Iterable[str] = None, + layer_norm: str | None = None, + inplace: bool = True, ) -> dict[str, np.ndarray] | None: """\ Wrap function from scanpy.pp.log1p diff --git a/stlearn/spatials/SME/_weighting_matrix.py b/stlearn/spatials/SME/_weighting_matrix.py index bd9c0bba..fcaa2f78 100644 --- a/stlearn/spatials/SME/_weighting_matrix.py +++ b/stlearn/spatials/SME/_weighting_matrix.py @@ -1,4 +1,3 @@ - import numpy as np from anndata import AnnData from sklearn.metrics import pairwise_distances diff --git a/stlearn/spatials/SME/impute.py b/stlearn/spatials/SME/impute.py index 109814bd..31f16467 100644 --- a/stlearn/spatials/SME/impute.py +++ b/stlearn/spatials/SME/impute.py @@ -18,11 +18,11 @@ def SME_impute0( - adata: AnnData, - use_data: str = "raw", - weights: _WEIGHTING_MATRIX = "weights_matrix_all", - platform: _PLATFORM = "Visium", - copy: bool = False, + adata: AnnData, + use_data: str = "raw", + weights: _WEIGHTING_MATRIX = "weights_matrix_all", + platform: _PLATFORM = "Visium", + copy: bool = False, ) -> AnnData | None: """\ using spatial location (S), tissue morphological feature (M) and gene @@ -87,13 +87,13 @@ def SME_impute0( def pseudo_spot( - adata: AnnData, - tile_path: Path | str = Path("/tmp/tiles"), - use_data: str = "raw", - crop_size: int = "auto", - platform: _PLATFORM = "Visium", - weights: _WEIGHTING_MATRIX = "weights_matrix_all", - copy: _COPY = "pseudo_spot_adata", + adata: AnnData, + tile_path: Path | str = Path("/tmp/tiles"), + use_data: str = "raw", + crop_size: int = "auto", + platform: _PLATFORM = "Visium", + weights: _WEIGHTING_MATRIX = "weights_matrix_all", + copy: _COPY = "pseudo_spot_adata", ) -> AnnData | None: """\ using spatial location (S), tissue morphological feature (M) and gene @@ -249,10 +249,10 @@ def pseudo_spot( reg_col = LinearRegression().fit(array_col.values.reshape(-1, 1), img_col) obs_df.loc[:, "imagerow"] = ( - obs_df.loc[:, "array_row"] * reg_row.coef_ + reg_row.intercept_ + obs_df.loc[:, "array_row"] * reg_row.coef_ + reg_row.intercept_ ) obs_df.loc[:, "imagecol"] = ( - obs_df.loc[:, "array_col"] * reg_col.coef_ + reg_col.intercept_ + obs_df.loc[:, "array_col"] * reg_col.coef_ + reg_col.intercept_ ) impute_coor = obs_df[["imagecol", "imagerow"]] @@ -260,7 +260,7 @@ def pseudo_spot( point_tree = scipy.spatial.cKDTree(coor) n_neighbour = [] - unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) for i in range(len(impute_coor)): current_neighbour = point_tree.query_ball_point( impute_coor.values[i], round(unit) @@ -319,9 +319,9 @@ def pseudo_spot( def _merge( - adata1: AnnData, - adata2: AnnData, - copy: bool = True, + adata1: AnnData, + adata2: AnnData, + copy: bool = True, ) -> AnnData | None: merged_df = adata1.to_df().append(adata2.to_df()) merged_df_obs = adata1.obs.append(adata2.obs) diff --git a/stlearn/spatials/SME/normalize.py b/stlearn/spatials/SME/normalize.py index 38849d37..04ef41e4 100644 --- a/stlearn/spatials/SME/normalize.py +++ b/stlearn/spatials/SME/normalize.py @@ -1,4 +1,3 @@ - import numpy as np import pandas as pd from anndata import AnnData @@ -13,11 +12,11 @@ def SME_normalize( - adata: AnnData, - use_data: str = "raw", - weights: _WEIGHTING_MATRIX = "weights_matrix_all", - platform: _PLATFORM = "Visium", - copy: bool = False, + adata: AnnData, + use_data: str = "raw", + weights: _WEIGHTING_MATRIX = "weights_matrix_all", + platform: _PLATFORM = "Visium", + copy: bool = False, ) -> AnnData | None: """\ using spatial location (S), tissue morphological feature (M) and gene diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index aaba5d66..1a9c2e3e 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -1,4 +1,3 @@ - import numpy as np import pandas as pd from anndata import AnnData @@ -7,11 +6,11 @@ def localization( - adata: AnnData, - use_label: str = "louvain", - eps: int = 20, - min_samples: int = 0, - copy: bool = False, + adata: AnnData, + use_label: str = "louvain", + eps: int = 20, + min_samples: int = 0, + copy: bool = False, ) -> AnnData | None: """\ Perform local cluster by using DBSCAN. diff --git a/stlearn/spatials/morphology/adjust.py b/stlearn/spatials/morphology/adjust.py index 039ce5f9..1305a3bb 100644 --- a/stlearn/spatials/morphology/adjust.py +++ b/stlearn/spatials/morphology/adjust.py @@ -1,4 +1,3 @@ - import numpy as np import scipy.spatial as spatial from anndata import AnnData diff --git a/stlearn/spatials/smooth/disk.py b/stlearn/spatials/smooth/disk.py index dabf0450..970d1843 100644 --- a/stlearn/spatials/smooth/disk.py +++ b/stlearn/spatials/smooth/disk.py @@ -1,16 +1,15 @@ - import numpy as np import scipy.spatial as spatial from anndata import AnnData def disk( - adata: AnnData, - use_data: str = "X_umap", - radius: float = 10.0, - rates: int = 1, - method: str = "mean", - copy: bool = False, + adata: AnnData, + use_data: str = "X_umap", + radius: float = 10.0, + rates: int = 1, + method: str = "mean", + copy: bool = False, ) -> AnnData | None: coor = adata.obs[["imagecol", "imagerow"]] count_embed = adata.obsm[use_data] @@ -46,8 +45,8 @@ def disk( adata.obsm[new_embed] = np.array(lag_coor) print( - 'Disk smoothing function is applied! The new data are stored in ' + - 'adata.obsm["X_diffmap_disk"]' + "Disk smoothing function is applied! The new data are stored in " + + 'adata.obsm["X_diffmap_disk"]' ) return adata if copy else None diff --git a/stlearn/spatials/trajectory/detect_transition_markers.py b/stlearn/spatials/trajectory/detect_transition_markers.py index d703894b..b538d147 100644 --- a/stlearn/spatials/trajectory/detect_transition_markers.py +++ b/stlearn/spatials/trajectory/detect_transition_markers.py @@ -10,12 +10,12 @@ def detect_transition_markers_clades( - adata, - clade, - cutoff_spearman=0.4, - cutoff_pvalue=0.05, - screening_genes=None, - use_raw_count=False, + adata, + clade, + cutoff_spearman=0.4, + cutoff_pvalue=0.05, + screening_genes=None, + use_raw_count=False, ): """\ Transition markers detection of a clade. @@ -69,7 +69,7 @@ def detect_transition_markers_clades( ) negative = spearman_result[ spearman_result["score"] <= cutoff_spearman * (-1) - ].sort_values("score") + ].sort_values("score") result = pd.concat([positive, negative]) @@ -81,12 +81,12 @@ def detect_transition_markers_clades( def detect_transition_markers_branches( - adata, - branch, - cutoff_spearman=0.4, - cutoff_pvalue=0.05, - screening_genes=None, - use_raw_count=False, + adata, + branch, + cutoff_spearman=0.4, + cutoff_pvalue=0.05, + screening_genes=None, + use_raw_count=False, ): """\ Transition markers detection of a branch. @@ -126,7 +126,7 @@ def detect_transition_markers_branches( ) negative = spearman_result[ spearman_result["score"] <= cutoff_spearman * (-1) - ].sort_values("score") + ].sort_values("score") result = pd.concat([positive, negative]) diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatials/trajectory/global_level.py index 8d7b4493..0cf96464 100644 --- a/stlearn/spatials/trajectory/global_level.py +++ b/stlearn/spatials/trajectory/global_level.py @@ -1,4 +1,3 @@ - import networkx as nx import numpy as np from anndata import AnnData @@ -8,15 +7,15 @@ def global_level( - adata: AnnData, - use_label: str = "louvain", - use_rep: str = "X_pca", - n_dims: int = 40, - list_clusters: list = [], - return_graph: bool = False, - w: float = None, - verbose: bool = True, - copy: bool = False, + adata: AnnData, + use_label: str = "louvain", + use_rep: str = "X_pca", + n_dims: int = 40, + list_clusters: list = [], + return_graph: bool = False, + w: float = None, + verbose: bool = True, + copy: bool = False, ) -> AnnData | None: """\ Perform global sptial trajectory inference. @@ -114,8 +113,8 @@ def global_level( H_sub = H.edge_subgraph(edge_list) if not nx.is_connected(H_sub.to_undirected()): raise ValueError( - "The chosen clusters are not available to construct the spatial " + - "trajectory! Please choose other path." + "The chosen clusters are not available to construct the spatial " + + "trajectory! Please choose other path." ) H_sub = nx.DiGraph(H_sub) prepare_root = [] @@ -268,6 +267,7 @@ def ge_distance_matrix(adata, cluster1, cluster2, use_label, use_rep, n_dims): return scale_sdm + # def _density_normalize(other: Union[np.ndarray, spmatrix] # ) -> Union[np.ndarray, spmatrix]: # """ diff --git a/stlearn/spatials/trajectory/local_level.py b/stlearn/spatials/trajectory/local_level.py index a5dacb76..bd791312 100644 --- a/stlearn/spatials/trajectory/local_level.py +++ b/stlearn/spatials/trajectory/local_level.py @@ -1,17 +1,16 @@ - import numpy as np from anndata import AnnData from scipy.spatial.distance import cdist def local_level( - adata: AnnData, - use_label: str = "louvain", - cluster: int = 9, - w: float = 0.5, - return_matrix: bool = False, - verbose: bool = True, - copy: bool = False, + adata: AnnData, + use_label: str = "louvain", + cluster: int = 9, + w: float = 0.5, + return_matrix: bool = False, + verbose: bool = True, + copy: bool = False, ) -> AnnData | None: """\ Perform local sptial trajectory inference (required run pseudotime first). @@ -49,8 +48,8 @@ def local_level( centroid_dict = {int(key): centroid_dict[key] for key in centroid_dict} for i in list_cluster: if ( - len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) - > adata.uns["threshold_spots"] + len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) + > adata.uns["threshold_spots"] ): dpt.append( cluster_data.obs[cluster_data.obs["sub_cluster_labels"] == i][ diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index 8ae61b74..035556b4 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -1,4 +1,3 @@ - import networkx as nx import numpy as np import pandas as pd @@ -7,21 +6,21 @@ def pseudotime( - adata: AnnData, - use_label: str = None, - eps: float = 20, - n_neighbors: int = 25, - use_rep: str = "X_pca", - threshold: float = 0.01, - radius: int = 50, - method: str = "mean", - threshold_spots: int = 5, - use_sme: bool = False, - reverse: bool = False, - pseudotime_key: str = "dpt_pseudotime", - max_nodes: int = 4, - run_knn: bool = False, - copy: bool = False, + adata: AnnData, + use_label: str = None, + eps: float = 20, + n_neighbors: int = 25, + use_rep: str = "X_pca", + threshold: float = 0.01, + radius: int = 50, + method: str = "mean", + threshold_spots: int = 5, + use_sme: bool = False, + reverse: bool = False, + pseudotime_key: str = "dpt_pseudotime", + max_nodes: int = 4, + run_knn: bool = False, + copy: bool = False, ) -> AnnData | None: """\ Perform pseudotime analysis. @@ -113,8 +112,8 @@ def pseudotime( "sub_cluster_labels" ].unique(): if ( - len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) - > threshold_spots + len(adata.obs[adata.obs["sub_cluster_labels"] == str(i)]) + > threshold_spots ): meaningful_sub.append(i) @@ -243,6 +242,8 @@ def store_available_paths(adata, threshold, use_label, max_nodes, pseudotime_key adata.uns["available_paths"] = all_paths print( - "All available trajectory paths are stored in adata.uns['available_paths'] " + - "with length < " + str(max_nodes) + " nodes" + "All available trajectory paths are stored in adata.uns['available_paths'] " + + "with length < " + + str(max_nodes) + + " nodes" ) diff --git a/stlearn/spatials/trajectory/pseudotimespace.py b/stlearn/spatials/trajectory/pseudotimespace.py index 7f362c1d..2214c401 100644 --- a/stlearn/spatials/trajectory/pseudotimespace.py +++ b/stlearn/spatials/trajectory/pseudotimespace.py @@ -1,4 +1,3 @@ - from anndata import AnnData from .global_level import global_level @@ -7,14 +6,14 @@ def pseudotimespace_global( - adata: AnnData, - use_label: str = "louvain", - use_rep: str = "X_pca", - n_dims: int = 40, - list_clusters=None, - model: str = "spatial", - step=0.01, - k=10, + adata: AnnData, + use_label: str = "louvain", + use_rep: str = "X_pca", + n_dims: int = 40, + list_clusters=None, + model: str = "spatial", + step=0.01, + k=10, ) -> AnnData | None: """\ Perform pseudo-time-space analysis with global level. @@ -56,8 +55,8 @@ def pseudotimespace_global( w = 1 else: raise ValueError( - "Please choose the right model! Available models: 'mixed', 'spatial' " + - "and 'gene_expression' " + "Please choose the right model! Available models: 'mixed', 'spatial' " + + "and 'gene_expression' " ) global_level( @@ -71,10 +70,10 @@ def pseudotimespace_global( def pseudotimespace_local( - adata: AnnData, - use_label: str = "louvain", - cluster=None, - w: float = None, + adata: AnnData, + use_label: str = "louvain", + cluster=None, + w: float = None, ) -> AnnData | None: """\ Perform pseudo-time-space analysis with local level. diff --git a/stlearn/spatials/trajectory/set_root.py b/stlearn/spatials/trajectory/set_root.py index 88f8251e..6d232589 100644 --- a/stlearn/spatials/trajectory/set_root.py +++ b/stlearn/spatials/trajectory/set_root.py @@ -28,8 +28,8 @@ def set_root(adata: AnnData, use_label: str, cluster: str, use_raw: bool = False # Subset the data based on the chosen cluster tmp_adata = tmp_adata[ - tmp_adata.obs[tmp_adata.obs[use_label] == str(cluster)].index, : - ] + tmp_adata.obs[tmp_adata.obs[use_label] == str(cluster)].index, : + ] if use_raw: tmp_adata = tmp_adata.raw.to_adata() diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatials/trajectory/utils.py index 8381cc74..1c06cc51 100644 --- a/stlearn/spatials/trajectory/utils.py +++ b/stlearn/spatials/trajectory/utils.py @@ -61,7 +61,7 @@ def lambda_dist(A1, A2, k=None, p=2, kind="laplacian"): def resistance_distance( - A1, A2, p=2, renormalized=False, attributed=False, check_connected=True, beta=1 + A1, A2, p=2, renormalized=False, attributed=False, check_connected=True, beta=1 ): """Compare two graphs using resistance distance (possibly renormalized). Parameters @@ -316,6 +316,7 @@ class UndefinedException(Exception): # Resistance matrix. Renormalized version, as well as conductance and commute matrices. # """ + def resistance_matrix(A, check_connected=True): """Return the resistance matrix of G. Parameters @@ -513,8 +514,8 @@ def conductance_matrix(A): # CytoTrace wrapper def _mat_mat_corr_sparse( - X: csr_matrix, - Y: np.ndarray, + X: csr_matrix, + Y: np.ndarray, ) -> np.ndarray: """\ This function is borrow from cellrank @@ -522,8 +523,7 @@ def _mat_mat_corr_sparse( n = X.shape[1] X_bar = np.reshape(np.array(X.mean(axis=1)), (-1, 1)) - X_std = np.reshape(np.sqrt(np.array(X.power(2).mean(axis=1)) - (X_bar ** 2)), - (-1, 1)) + X_std = np.reshape(np.sqrt(np.array(X.power(2).mean(axis=1)) - (X_bar**2)), (-1, 1)) y_bar = np.reshape(np.mean(Y, axis=0), (1, -1)) y_std = np.reshape(np.std(Y, axis=0), (1, -1)) @@ -536,12 +536,12 @@ def _mat_mat_corr_sparse( def _correlation_test_helper( - X: np.ndarray | spmatrix, - Y: np.ndarray, - n_perms: int | None = None, - seed: int | None = None, - confidence_level: float = 0.95, - **kwargs, + X: np.ndarray | spmatrix, + Y: np.ndarray, + n_perms: int | None = None, + seed: int | None = None, + confidence_level: float = 0.95, + **kwargs, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ This function is borrow from cellrank. @@ -569,7 +569,7 @@ def _correlation_test_helper( """ def perm_test_extractor( - res: Sequence[tuple[np.ndarray, np.ndarray]], + res: Sequence[tuple[np.ndarray, np.ndarray]], ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: pvals, corr_bs = zip(*res) pvals = np.sum(pvals, axis=0) / float(n_perms) @@ -583,8 +583,8 @@ def perm_test_extractor( if not (0 <= confidence_level <= 1): raise ValueError( - "Expected `confidence_level` to be in interval `[0, 1]`, " + - f"found `{confidence_level}`." + "Expected `confidence_level` to be in interval `[0, 1]`, " + + f"found `{confidence_level}`." ) n = X.shape[1] # genes x cells diff --git a/stlearn/spatials/trajectory/weight_optimization.py b/stlearn/spatials/trajectory/weight_optimization.py index 11e09e9d..9787d628 100644 --- a/stlearn/spatials/trajectory/weight_optimization.py +++ b/stlearn/spatials/trajectory/weight_optimization.py @@ -9,13 +9,13 @@ def weight_optimizing_global( - adata, - use_label=None, - list_clusters=None, - step=0.01, - k=10, - use_rep="X_pca", - n_dims=40, + adata, + use_label=None, + list_clusters=None, + step=0.01, + k=10, + use_rep="X_pca", + n_dims=40, ): # Screening PTS graph print("Screening PTS global graph...") @@ -23,9 +23,9 @@ def weight_optimizing_global( j = 0 with tqdm( - total=int(1 / step + 1), - desc="Screening", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=int(1 / step + 1), + desc="Screening", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(0, int(1 / step + 1)): Gs.append( @@ -59,9 +59,9 @@ def weight_optimizing_global( ].unique() ) with tqdm( - total=int(1 / step - 1), - desc="Calculating", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=int(1 / step - 1), + desc="Calculating", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(1, int(1 / step)): w += step @@ -100,9 +100,9 @@ def weight_optimizing_local(adata, use_label=None, cluster=None, step=0.01): Gs = [] j = 0 with tqdm( - total=int(1 / step + 1), - desc="Screening", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=int(1 / step + 1), + desc="Screening", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(0, int(1 / step + 1)): Gs.append( @@ -128,9 +128,9 @@ def weight_optimizing_local(adata, use_label=None, cluster=None, step=0.01): w = 0 with tqdm( - total=int(1 / step - 1), - desc="Calculating", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=int(1 / step - 1), + desc="Calculating", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(1, int(1 / step)): w += step diff --git a/stlearn/tools/clustering/kmeans.py b/stlearn/tools/clustering/kmeans.py index c436e3c2..398e4184 100644 --- a/stlearn/tools/clustering/kmeans.py +++ b/stlearn/tools/clustering/kmeans.py @@ -1,4 +1,3 @@ - import numpy as np import pandas as pd from anndata import AnnData @@ -7,18 +6,18 @@ def kmeans( - adata: AnnData, - n_clusters: int = 20, - use_data: str = "X_pca", - init: str = "k-means++", - n_init: int = 10, - max_iter: int = 300, - tol: float = 0.0001, - random_state: str = None, - copy_x: bool = True, - algorithm: str = "auto", - key_added: str = "kmeans", - copy: bool = False, + adata: AnnData, + n_clusters: int = 20, + use_data: str = "X_pca", + init: str = "k-means++", + n_init: int = 10, + max_iter: int = 300, + tol: float = 0.0001, + random_state: str = None, + copy_x: bool = True, + algorithm: str = "auto", + key_added: str = "kmeans", + copy: bool = False, ) -> AnnData | None: """\ Perform kmeans cluster for spatial transcriptomics data diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index 87d7700d..ba52ae47 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -20,18 +20,18 @@ class MutableVertexPartition: def louvain( - adata: AnnData, - resolution: float | None = None, - random_state: int | RandomState | None = 0, - restrict_to: tuple[str, Sequence[str]] | None = None, - key_added: str = "louvain", - adjacency: spmatrix | None = None, - flavor: Literal["vtraag", "igraph", "rapids"] = "vtraag", # noqa: F821 - directed: bool = True, - use_weights: bool = False, - partition_type: type[MutableVertexPartition] | None = None, - partition_kwargs: Mapping[str, Any] = MappingProxyType({}), - copy: bool = False, + adata: AnnData, + resolution: float | None = None, + random_state: int | RandomState | None = 0, + restrict_to: tuple[str, Sequence[str]] | None = None, + key_added: str = "louvain", + adjacency: spmatrix | None = None, + flavor: Literal["vtraag", "igraph", "rapids"] = "vtraag", # noqa: F821 + directed: bool = True, + use_weights: bool = False, + partition_type: type[MutableVertexPartition] | None = None, + partition_kwargs: Mapping[str, Any] = MappingProxyType({}), + copy: bool = False, ) -> AnnData | None: """\ Wrap function scanpy.tl.louvain diff --git a/stlearn/tools/label/label.py b/stlearn/tools/label/label.py index 9dea9a20..8e95039a 100644 --- a/stlearn/tools/label/label.py +++ b/stlearn/tools/label/label.py @@ -11,8 +11,7 @@ def run_label_transfer( - st_data, sc_data, sc_label_col, r_path, st_label_col=None, - n_highly_variable=2000 + st_data, sc_data, sc_label_col, r_path, st_label_col=None, n_highly_variable=2000 ): """Runs Seurat label transfer.""" st_label_col = sc_label_col if st_label_col is None else st_label_col @@ -96,15 +95,15 @@ def get_counts(data): elif data.X is np.ndarray and np.all(np.mod(data.X[0, :], 1) == 0): counts = data.to_df().transpose() elif ( - data.X is not np.ndarray - and hasattr(data, "raw") - and np.all(np.mod(data.raw.X[0, :].todense(), 1) == 0) + data.X is not np.ndarray + and hasattr(data, "raw") + and np.all(np.mod(data.raw.X[0, :].todense(), 1) == 0) ): counts = data.raw.to_adata()[data.obs_names, data.var_names].to_df().transpose() elif ( - data.X is np.ndarray - and hasattr(data, "raw") - and np.all(np.mod(data.raw.X[0, :], 1) == 0) + data.X is np.ndarray + and hasattr(data, "raw") + and np.all(np.mod(data.raw.X[0, :], 1) == 0) ): counts = data.raw.to_adata()[data.obs_names, data.var_names].to_df().transpose() else: @@ -117,15 +116,15 @@ def get_counts(data): def run_rctd( - st_data, - sc_data, - sc_label_col, - r_path, - st_label_col=None, - n_highly_variable=5000, - min_cells=10, - doublet_mode="full", - n_cores=1, + st_data, + sc_data, + sc_label_col, + r_path, + st_label_col=None, + n_highly_variable=5000, + min_cells=10, + doublet_mode="full", + n_cores=1, ): """Runs RCTD for deconvolution.""" st_label_col = sc_label_col if st_label_col is None else st_label_col @@ -202,23 +201,23 @@ def run_rctd( st_data_orig.obs[st_label_col] = labels st_data_orig.obs[st_label_col] = st_data_orig.obs[st_label_col].astype("category") st_data_orig.uns[st_label_col] = rctd_proportions.loc[ - st_data_orig.obs_names.values, : - ] + st_data_orig.obs_names.values, : + ] print(f"Spot labels added to st_data.obs[{st_label_col}].") print(f"Spot label scores added to st_data.uns[{st_label_col}].") def run_singleR( - st_data, - sc_data, - sc_label_col, - r_path, - st_label_col=None, - n_highly_variable=5000, - n_centers=3, - de_n=200, - de_method="t", + st_data, + sc_data, + sc_label_col, + r_path, + st_label_col=None, + n_highly_variable=5000, + n_centers=3, + de_n=200, + de_method="t", ): """Runs SingleR spot annotation.""" st_label_col = sc_label_col if st_label_col is None else st_label_col diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index 81ec454d..16d21cad 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -686,8 +686,8 @@ def run_cci( lr_n_cci_sig = np.zeros(lr_summary.shape[0]) with tqdm( total=len(best_lrs), - desc="Counting celltype-celltype interactions per LR and permuting " + - f"{n_perms} times.", + desc="Counting celltype-celltype interactions per LR and permuting " + + f"{n_perms} times.", bar_format="{l_bar}{bar} [ time left: {remaining} ]", disable=verbose is False, ) as pbar: diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tools/microenv/cci/base.py index 6f88750e..edbc5c66 100644 --- a/stlearn/tools/microenv/cci/base.py +++ b/stlearn/tools/microenv/cci/base.py @@ -10,12 +10,12 @@ def lr( - adata: AnnData, - use_lr: str = "cci_lr", - distance: float = None, - verbose: bool = True, - neighbours: list = None, - fast: bool = True, + adata: AnnData, + use_lr: str = "cci_lr", + distance: float = None, + verbose: bool = True, + neighbours: list = None, + fast: bool = True, ) -> AnnData: """Calculate the proportion of known ligand-receptor co-expression among the neighbouring spots or within spots @@ -92,24 +92,23 @@ def calc_distance(adata: AnnData, distance: float): scalefactors = next(iter(adata.uns["spatial"].values()))["scalefactors"] library_id = list(adata.uns["spatial"].keys())[0] distance = ( - scalefactors["spot_diameter_fullres"] - * scalefactors[ - "tissue_" + adata.uns["spatial"][library_id][ - "use_quality"] + "_scalef" - ] - * 2 + scalefactors["spot_diameter_fullres"] + * scalefactors[ + "tissue_" + adata.uns["spatial"][library_id]["use_quality"] + "_scalef" + ] + * 2 ) return distance def get_lrs_scores( - adata: AnnData, - lrs: np.array, - neighbours: np.array, - het_vals: np.array, - min_expr: float, - filter_pairs: bool = True, - spot_indices: np.array = None, + adata: AnnData, + lrs: np.array, + neighbours: np.array, + het_vals: np.array, + min_expr: float, + filter_pairs: bool = True, + spot_indices: np.array = None, ): """Gets the scores for the indicated set of LR pairs & the heterogeneity values. Parameters @@ -145,7 +144,7 @@ def get_lrs_scores( if filter_pairs: lrs = np.array( [ - "_".join(spot_lr1s.columns.values[i: i + 2]) + "_".join(spot_lr1s.columns.values[i : i + 2]) for i in range(0, spot_lr1s.shape[1], 2) ] ) @@ -162,10 +161,10 @@ def get_lrs_scores( def get_spot_lrs( - adata: AnnData, - lr_pairs: list, - lr_order: bool, - filter_pairs: bool = True, + adata: AnnData, + lr_pairs: list, + lr_order: bool, + filter_pairs: bool = True, ): """ Parameters @@ -204,10 +203,10 @@ def get_spot_lrs( def calc_neighbours( - adata: AnnData, - distance: float = None, - index: bool = True, - verbose: bool = True, + adata: AnnData, + distance: float = None, + index: bool = True, + verbose: bool = True, ) -> List: """Calculate the proportion of known ligand-receptor co-expression among the neighbouring spots or within spots @@ -273,11 +272,11 @@ def calc_neighbours( @njit def lr_core( - spot_lr1: np.ndarray, - spot_lr2: np.ndarray, - neighbours: List, - min_expr: float, - spot_indices: np.array, + spot_lr1: np.ndarray, + spot_lr2: np.ndarray, + neighbours: List, + min_expr: float, + spot_indices: np.array, ) -> np.ndarray: """Calculate the lr scores for each spot. Parameters @@ -307,17 +306,17 @@ def lr_core( nb_lr2[i, :] = nb_expr_mean scores = ( - spot_lr1[spot_indices, :] * (nb_lr2 > min_expr) - + (spot_lr1[spot_indices, :] > min_expr) * nb_lr2 + spot_lr1[spot_indices, :] * (nb_lr2 > min_expr) + + (spot_lr1[spot_indices, :] > min_expr) * nb_lr2 ) spot_lr = scores.sum(axis=1) return spot_lr / 2 def lr_pandas( - spot_lr1: np.ndarray, - spot_lr2: np.ndarray, - neighbours: list, + spot_lr1: np.ndarray, + spot_lr2: np.ndarray, + neighbours: list, ) -> np.ndarray: """Calculate the lr scores for each spot. Parameters @@ -363,12 +362,12 @@ def mean_lr2(x): @njit(parallel=True) def get_scores( - spot_lr1s: np.ndarray, - spot_lr2s: np.ndarray, - neighbours: List, - het_vals: np.array, - min_expr: float, - spot_indices: np.array, + spot_lr1s: np.ndarray, + spot_lr2s: np.ndarray, + neighbours: List, + het_vals: np.array, + min_expr: float, + spot_indices: np.array, ) -> np.array: """Calculates the scores. Parameters @@ -391,7 +390,7 @@ def get_scores( spot_scores = np.zeros((len(spot_indices), spot_lr1s.shape[1] // 2), np.float64) for i in prange(0, spot_lr1s.shape[1] // 2): i_ = i * 2 # equivalent to range(0, spot_lr1s.shape[1], 2) - spot_lr1, spot_lr2 = spot_lr1s[:, i_: (i_ + 2)], spot_lr2s[:, i_: (i_ + 2)] + spot_lr1, spot_lr2 = spot_lr1s[:, i_ : (i_ + 2)], spot_lr2s[:, i_ : (i_ + 2)] lr_scores = lr_core(spot_lr1, spot_lr2, neighbours, min_expr, spot_indices) # The merge scores # lr_scores = np.multiply(het_vals[spot_indices], lr_scores) @@ -400,12 +399,12 @@ def get_scores( def lr_grid( - adata: AnnData, - num_row: int = 10, - num_col: int = 10, - use_lr: str = "cci_lr_grid", - radius: int = 1, - verbose: bool = True, + adata: AnnData, + num_row: int = 10, + num_col: int = 10, + use_lr: str = "cci_lr_grid", + radius: int = 1, + verbose: bool = True, ) -> AnnData: """Calculate the proportion of known ligand-receptor co-expression among the neighbouring grids or within each grid @@ -451,7 +450,7 @@ def lr_grid( & (coor["imagecol"] < grid[0] + width) & (coor["imagerow"] < grid[1]) & (coor["imagerow"] > grid[1] - height) - ] + ] df_grid.loc[n] = df.loc[spots.index].sum() # expand the LR pairs list by swapping ligand-receptor positions diff --git a/stlearn/tools/microenv/cci/base_grouping.py b/stlearn/tools/microenv/cci/base_grouping.py index b8a05e7a..24201a71 100644 --- a/stlearn/tools/microenv/cci/base_grouping.py +++ b/stlearn/tools/microenv/cci/base_grouping.py @@ -14,14 +14,14 @@ def get_hotspots( - adata: AnnData, - lr_scores: np.ndarray, - lrs: np.array, - eps: float, - quantile=0.05, - verbose=True, - plot_diagnostics: bool = False, - show_plot: bool = False, + adata: AnnData, + lr_scores: np.ndarray, + lrs: np.array, + eps: float, + quantile=0.05, + verbose=True, + plot_diagnostics: bool = False, + show_plot: bool = False, ): """Determines the hotspots for the inputted scores by progressively setting more stringent cutoffs & cluster in space, chooses point which maximises number @@ -120,23 +120,23 @@ def get_hotspots( if verbose: print("\tSummary values of lrs in adata.uns['lr_summary'].") print( - "\tMatrix of lr scores in same order as the summary in " + - "adata.obsm['lr_scores']." + "\tMatrix of lr scores in same order as the summary in " + + "adata.obsm['lr_scores']." ) print("\tMatrix of the hotspot scores in adata.obsm['lr_hot_scores'].") print("\tMatrix of the mean LR cluster scores in adata.obsm['cluster_scores'].") def hotspot_core( - lr_scores, - lrs, - coors, - eps, - quantile, - plot_diagnostics=False, - adata=None, - verbose=True, - max_score=False, + lr_scores, + lrs, + coors, + eps, + quantile, + plot_diagnostics=False, + adata=None, + verbose=True, + max_score=False, ): """Made code for getting the hotspot information.""" score_copy = lr_scores.copy() @@ -159,10 +159,10 @@ def hotspot_core( # Determining the cutoffs for hotspots # with tqdm( - total=len(lrs), - desc="Removing background lr scores...", - bar_format="{l_bar}{bar}", - disable=verbose is False, + total=len(lrs), + desc="Removing background lr scores...", + bar_format="{l_bar}{bar}", + disable=verbose is False, ) as pbar: for i, lr_ in enumerate(lrs): lr_score_ = score_copy[i, :] @@ -221,17 +221,17 @@ def non_zero_mean(vals): def add_diagnostic_plots( - adata, - i, - lr_, - quant_lrs, - lr_quantiles, - lr_scores, - lr_hot_scores, - axes, - cutoffs, - n_clusters, - best_cutoff, + adata, + i, + lr_, + quant_lrs, + lr_quantiles, + lr_scores, + lr_hot_scores, + axes, + cutoffs, + n_clusters, + best_cutoff, ): """Adds diagnostic plots for the quantile LR pair to a figure to illustrate \ how the cutoff is functioning. diff --git a/stlearn/tools/microenv/cci/het_helpers.py b/stlearn/tools/microenv/cci/het_helpers.py index c1b76f9b..67386fc2 100644 --- a/stlearn/tools/microenv/cci/het_helpers.py +++ b/stlearn/tools/microenv/cci/het_helpers.py @@ -9,13 +9,13 @@ @njit def edge_core( - cell_data: np.ndarray, - cell_type_index: int, - neighbourhood_bcs: List, - neighbourhood_indices: List, - spot_indices: np.array = None, - neigh_bool: np.array = None, - cutoff: float = 0.2, + cell_data: np.ndarray, + cell_type_index: int, + neighbourhood_bcs: List, + neighbourhood_indices: List, + spot_indices: np.array = None, + neigh_bool: np.array = None, + cutoff: float = 0.2, ) -> np.array: """Gets the edges which connect inputted spots to neighbours of a given cell type. @@ -128,12 +128,12 @@ def init_edge_list(neighbourhood_bcs): @njit def get_between_spot_edge_array( - edge_list: List, - neighbourhood_bcs: List, - neighbourhood_indices: List, - neigh_bool: np.array, - cell_data: np.array, - cutoff: float = 0, + edge_list: List, + neighbourhood_bcs: List, + neighbourhood_indices: List, + neigh_bool: np.array, + cell_data: np.array, + cutoff: float = 0, ): """ Populates edge_list with edges linking spots with a valid neighbour \ of a given cell type. Validity of neighbour determined by neigh_bool, \ @@ -184,7 +184,7 @@ def add_unique_edges(edge_list, edge_starts, edge_ends): edge_startj, edge_endj = edge_starts[j], edge_ends[j] # Direction doesn't matter # if (edge_start == edge_startj and edge_end == edge_endj) or ( - edge_end == edge_startj and edge_start == edge_endj + edge_end == edge_startj and edge_start == edge_endj ): edge_added[j] = True @@ -261,12 +261,12 @@ def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): # @njit def get_neighbourhoods_FAST( - spot_bcs: np.array, - spot_neigh_bcs: np.ndarray, - n_spots: int, - str_dtype: str, - neigh_indices: np.array, - neigh_bcs: np.array, + spot_bcs: np.array, + spot_neigh_bcs: np.ndarray, + n_spots: int, + str_dtype: str, + neigh_indices: np.array, + neigh_bcs: np.array, ): """Gets the neighbourhood information, njit compiled.""" @@ -347,12 +347,12 @@ def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): def get_neighbourhoods_FAST( - spot_bcs: np.array, - spot_neigh_bcs: np.ndarray, - n_spots: int, - str_dtype: str, - neigh_indices: np.array, - neigh_bcs: np.array, + spot_bcs: np.array, + spot_neigh_bcs: np.ndarray, + n_spots: int, + str_dtype: str, + neigh_indices: np.array, + neigh_bcs: np.array, ): """Gets the neighbourhood information, njit compiled.""" diff --git a/stlearn/tools/microenv/cci/merge.py b/stlearn/tools/microenv/cci/merge.py index 15017fe1..4eb91dc4 100644 --- a/stlearn/tools/microenv/cci/merge.py +++ b/stlearn/tools/microenv/cci/merge.py @@ -3,10 +3,10 @@ def merge( - adata: AnnData, - use_lr: str = "cci_lr", - use_het: str = "cci_het", - verbose: bool = True, + adata: AnnData, + use_lr: str = "cci_lr", + use_het: str = "cci_het", + verbose: bool = True, ) -> AnnData: """Merge results from cell type heterogeneity and L-R cluster Parameters @@ -24,8 +24,8 @@ def merge( if verbose: print( - "Results of spatial interaction analysis has been written to " + - "adata.uns['merged']" + "Results of spatial interaction analysis has been written to " + + "adata.uns['merged']" ) return adata diff --git a/stlearn/tools/microenv/cci/permutation.py b/stlearn/tools/microenv/cci/permutation.py index 6abebe31..79b118c3 100644 --- a/stlearn/tools/microenv/cci/permutation.py +++ b/stlearn/tools/microenv/cci/permutation.py @@ -19,20 +19,19 @@ # Newest method # def perform_spot_testing( - adata: AnnData, - lr_scores: np.ndarray, - lrs: np.array, - n_pairs: int, - neighbours: List, - het_vals: np.array, - min_expr: float, - adj_method: str = "fdr_bh", - pval_adj_cutoff: float = 0.05, - verbose: bool = True, - save_bg=False, - neg_binom=False, - quantiles=( - 0.5, 0.75, 0.85, 0.9, 0.95, 0.97, 0.98, 0.99, 0.995, 0.9975, 0.999, 1), + adata: AnnData, + lr_scores: np.ndarray, + lrs: np.array, + n_pairs: int, + neighbours: List, + het_vals: np.array, + min_expr: float, + adj_method: str = "fdr_bh", + pval_adj_cutoff: float = 0.05, + verbose: bool = True, + save_bg=False, + neg_binom=False, + quantiles=(0.5, 0.75, 0.85, 0.9, 0.95, 0.97, 0.98, 0.99, 0.995, 0.9975, 0.999, 1), ): """Calls significant spots by creating random gene pairs with similar expression to given LR pair; only generate background for spots @@ -91,10 +90,10 @@ def perform_spot_testing( adata.uns["lr_spot_indices"] = {} with tqdm( - total=lr_scores.shape[1], - desc="Generating backgrounds & testing each LR pair...", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", - disable=verbose is False, + total=lr_scores.shape[1], + desc="Generating backgrounds & testing each LR pair...", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", + disable=verbose is False, ) as pbar: gene_bg_genes = {} # Keep track of genes which can be used to gen. rand-pairs. @@ -143,8 +142,8 @@ def perform_spot_testing( # First multiple to get minimum value to be one before rounding # bg_1 = bg_wScore * (1 / min(bg_wScore[bg_wScore != 0])) bg_1 = np.round(bg_1) - lr_j_scores_1 = bg_1[0: len(lr_j_scores)] - bg_1 = bg_1[len(lr_j_scores): len(bg_1)] + lr_j_scores_1 = bg_1[0 : len(lr_j_scores)] + bg_1 = bg_1[len(lr_j_scores) : len(bg_1)] # Getting the pvalue from negative binomial approach round_pvals, _, _, _ = get_stats( @@ -213,18 +212,18 @@ def perform_spot_testing( # Version 2, no longer in use, see above for newest method # def perform_perm_testing( - adata: AnnData, - lr_scores: np.ndarray, - n_pairs: int, - lrs: np.array, - lr_mid_dist: int, - verbose: float, - neighbours: List, - het_vals: np.array, - min_expr: float, - neg_binom: bool, - adj_method: str, - pval_adj_cutoff: float, + adata: AnnData, + lr_scores: np.ndarray, + n_pairs: int, + lrs: np.array, + lr_mid_dist: int, + verbose: float, + neighbours: List, + het_vals: np.array, + min_expr: float, + neg_binom: bool, + adj_method: str, + pval_adj_cutoff: float, ): """Performs the grouped permutation testing when taking the stats approach.""" if n_pairs != 0: # Perform permutation testing @@ -263,9 +262,9 @@ def perform_perm_testing( n_, n_sigs = np.array([0] * len(lrs)), np.array([0] * len(lrs)) per_lr_results = {} with tqdm( - total=len(lr_group_set), - desc="Generating background distributions for the LR pair groups..", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=len(lr_group_set), + desc="Generating background distributions for the LR pair groups..", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for group in lr_group_set: # Determining common mid-point for each group # @@ -335,25 +334,25 @@ def perform_perm_testing( "Summary of significant spots for each lr pair in adata.uns['lr_summary']." ) print( - "Spot enrichment statistics of LR interactions in " + - "adata.uns['per_lr_results']" + "Spot enrichment statistics of LR interactions in " + + "adata.uns['per_lr_results']" ) # No longer in use # def permutation( - adata: AnnData, - n_pairs: int = 200, - distance: int = None, - use_lr: str = "cci_lr", - use_het: str = None, - neg_binom: bool = False, - adj_method: str = "fdr", - neighbours: list = None, - run_fast: bool = True, - bg_pairs: list = None, - background: np.array = None, - **kwargs, + adata: AnnData, + n_pairs: int = 200, + distance: int = None, + use_lr: str = "cci_lr", + use_het: str = None, + neg_binom: bool = False, + adj_method: str = "fdr", + neighbours: list = None, + run_fast: bool = True, + bg_pairs: list = None, + background: np.array = None, + **kwargs, ) -> AnnData: """Permutation test for merged result Parameters @@ -476,13 +475,13 @@ def permutation( def get_stats( - scores: np.array, - background: np.array, - total_bg: int, - neg_binom: bool = False, - adj_method: str = "fdr_bh", - pval_adj_cutoff: float = 0.01, - return_negbinom_params: bool = False, + scores: np.array, + background: np.array, + total_bg: int, + neg_binom: bool = False, + adj_method: str = "fdr_bh", + pval_adj_cutoff: float = 0.01, + return_negbinom_params: bool = False, ): """Retrieves valid candidate genes to be used for random gene pairs. Parameters @@ -514,7 +513,7 @@ def get_stats( mu = np.exp(res.params[0]) alpha = res.params[1] Q = 0 - size = 1.0 / alpha * mu ** Q + size = 1.0 / alpha * mu**Q prob = size / (size + mu) if return_negbinom_params: # For testing purposes # @@ -575,11 +574,11 @@ def get_valid_genes(adata: AnnData, n_pairs: int) -> np.array: def get_rand_pairs( - adata: AnnData, - genes: np.array, - n_pairs: int, - lrs: list = None, - im: int = None, + adata: AnnData, + genes: np.array, + n_pairs: int, + lrs: list = None, + im: int = None, ): """Gets equivalent random gene pairs for the inputted lr pair. Parameters @@ -610,7 +609,7 @@ def get_rand_pairs( .drop(lr_genes)[: n_pairs * 2] .index.tolist() ) - selected = selected[0: n_pairs * 2] + selected = selected[0 : n_pairs * 2] adata.uns["selected"] = selected # form gene pairs from selected randomly random.shuffle(selected) @@ -626,7 +625,7 @@ def get_ordered(adata, genes): def get_median_index(ligand, receptor, means_ordered, genes_ordered): - """ Retrieves the index of the gene with a mean expression between the two genes + """Retrieves the index of the gene with a mean expression between the two genes in the lr pair. Parameters ---------- diff --git a/stlearn/utils.py b/stlearn/utils.py index b658fe26..0a76aec7 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -20,9 +20,7 @@ class _AxesSubplot(Axes, axes.SubplotBase): """Intersection between Axes and SubplotBase: Has methods of both""" -def _check_spot_size( - spatial_data: Mapping | None, spot_size: float | None -) -> float: +def _check_spot_size(spatial_data: Mapping | None, spot_size: float | None) -> float: """ Resolve spot_size value. This is a required argument for spatial plots. diff --git a/stlearn/wrapper/convert_scanpy.py b/stlearn/wrapper/convert_scanpy.py index 2f11e047..94a74ded 100644 --- a/stlearn/wrapper/convert_scanpy.py +++ b/stlearn/wrapper/convert_scanpy.py @@ -1,4 +1,3 @@ - from anndata import AnnData From cc5a7384f108ae52be0f0aa251c191b415a753e9 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 4 Jun 2025 09:53:21 +1000 Subject: [PATCH 027/241] Fix types. --- stlearn/plotting/cci_plot.py | 125 ++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 4cc3467f..3355d548 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -2,7 +2,7 @@ import math import sys from typing import ( - Optional, # Special + Optional, Any, # Special ) import matplotlib @@ -10,12 +10,14 @@ import networkx as nx import numpy as np import pandas as pd +import matplotlib as plt +import matplotlib.axes as plt_axis +import matplotlib.figure as plt_figure from anndata import AnnData from bokeh.io import output_notebook from bokeh.plotting import show -from matplotlib import pyplot as plt -from matplotlib.axes import Axes -from matplotlib.figure import Figure +from numpy.typing import NDArray + from scipy.stats import gaussian_kde import stlearn.plotting.cci_plot_helpers as cci_hs @@ -59,18 +61,18 @@ def lr_diagnostics( Parameters ---------- - adata: AnnData + adata (AnnData): The data object on which st.tl.cci.run has been applied. - highlight_lrs: list + highlight_lrs (list): List of LRs to highlight, will add text and change point color for these LR pairs. - n_top: int + n_top (int): The number of LRs to display. If None shows all. - color0: str + color0 (str): The color of the nonzero-median scatter plot. - lr_text_fp: dict + lr_text_fp (dict): Font dict for the LR text if highlight_lrs not None. - axis_text_fp: dict + axis_text_fp (dict): Font dict for the axis text labels. Returns ------- @@ -79,7 +81,7 @@ def lr_diagnostics( """ if n_top is None: n_top = adata.uns["lr_summary"].shape[0] - fig, axes = plt.subplots(ncols=2, figsize=figsize) + fig, axes = plt.pyplot.subplots(ncols=2, figsize=figsize) cci_hs.lr_scatter( adata, "nonzero-median", @@ -101,7 +103,7 @@ def lr_diagnostics( show=False, ) if show: - plt.show() + plt.pyplot.show() else: return fig, axes @@ -116,7 +118,7 @@ def lr_summary( highlight_color: str = "red", max_text: int = 50, lr_text_fp: dict = None, - ax: Axes = None, + ax: plt_axis.Axes = None, show: bool = True, ): """Plotting the top LRs ranked by number of significant spots. @@ -230,7 +232,7 @@ def lr_n_spots( n_sig = adata.uns["lr_summary"].loc[:, "n_spots_sig"].values n_non_sig = adata.uns["lr_summary"].loc[:, "n_spots"].values - n_sig rank = list(range(len(n_sig))) - fig, ax = plt.subplots(figsize=figsize) + fig, ax = plt.pyplot.subplots(figsize=figsize) ax.bar(rank[0:n_top], n_non_sig[0:n_top], bar_width, color=non_sig_color) ax.bar( rank[0:n_top], @@ -251,7 +253,7 @@ def lr_n_spots( ax.spines["right"].set_visible(False) if show: - plt.show() + plt.pyplot.show() else: return fig, ax @@ -387,7 +389,7 @@ def cci_check( label_set = label_set[order] # Plotting bar plot # - fig, ax = plt.subplots(figsize=figsize) + fig, ax = plt.pyplot.subplots(figsize=figsize) ax.bar(xs, cell_counts, color=colors) text_dist = max(cell_counts) * 0.015 fontdict = {"fontweight": "bold", "fontsize": cell_label_size} @@ -415,7 +417,7 @@ def cci_check( fig.tight_layout() if show: - plt.show() + plt.pyplot.show() else: return fig, ax, ax2 @@ -429,7 +431,7 @@ def lr_result_plot( title: Optional["str"] = None, figsize: tuple[float, float] | None = None, cmap: str | None = "Spectral_r", - ax: matplotlib.axes.Axes | None = None, + ax: plt_axis.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool | None = True, show_axis: bool | None = False, @@ -555,8 +557,8 @@ def lr_plot( title="", show_image: bool = True, show_arrows: bool = False, - fig: Figure = None, - ax: Axes = None, + fig: plt_figure.Figure = None, + ax: plt_axis.Axes = None, arrow_head_width: float = 4, arrow_width: float = 0.001, arrow_cmap: str = None, @@ -909,7 +911,7 @@ def het_plot( cmap: str | None = "Spectral_r", use_label: str | None = None, list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, + ax: plt_axis.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool | None = True, show_axis: bool | None = False, @@ -988,8 +990,8 @@ def het_plot( def ccinet_plot( adata: AnnData, use_label: str, - lr: str = None, - pos: dict = None, + lr: str | None = None, + pos: dict | None = None, return_pos: bool = False, cmap: str = "default", font_size: int = 12, @@ -997,10 +999,10 @@ def ccinet_plot( node_size_scaler: int = 1, min_counts: int = 0, sig_interactions: bool = True, - fig: matplotlib.figure.Figure = None, - ax: matplotlib.axes.Axes = None, + fig_or_none: plt_figure.Figure | None = None, + ax_or_none: plt_axis.Axes | None = None, pad=0.25, - title: str = None, + title_or_none: str | None = None, figsize: tuple = (10, 10), ): """Circular celltype-celltype interaction network based on LR-CCI analysis. @@ -1059,7 +1061,7 @@ def ccinet_plot( ) # Either plotting overall interactions, or just for a particular LR # - int_df, title = get_int_df(adata, lr, use_label, sig_interactions, title) + int_df, title = get_int_df(adata, lr, use_label, sig_interactions, title_or_none) # Creating the interaction graph # all_set = int_df.index.values int_matrix = int_df.values @@ -1113,8 +1115,10 @@ def ccinet_plot( node_colors = np.array(node_colors)[nodes_indices] #### Drawing the graph ##### - if fig is None or ax is None: - fig, ax = plt.subplots(figsize=figsize, facecolor=[0.7, 0.7, 0.7, 0.4]) + ax: plt_axis.Axes + fig: plt_figure.Figure + if fig_or_none is None or ax_or_none is None: + fig, ax = plt.pyplot.subplots(figsize=figsize, facecolor=[0.7, 0.7, 0.7, 0.4]) # Adding in the self-loops # z = 55 @@ -1131,7 +1135,7 @@ def ccinet_plot( width=0.3, height=0.025, lw=5, - ec=plt.cm.get_cmap("Blues")(edge_weights[i]), + ec=plt.colormaps.get_cmap("Blues")(edge_weights[i]), angle=angle, theta1=z, theta2=360 - z, @@ -1139,7 +1143,7 @@ def ccinet_plot( ax.add_patch(arc) # Drawing the main components of the graph # - edges = nx.draw_networkx( + nx.draw_networkx( graph, pos, node_size=node_sizes, @@ -1150,11 +1154,11 @@ def ccinet_plot( font_size=font_size, font_weight="bold", edge_color=edge_weights, - edge_cmap=plt.cm.Blues, + edge_cmap=plt.colormaps.get_cmap("Blues"), ax=ax, ) fig.suptitle(title, fontsize=30) - plt.tight_layout() + plt.pyplot.tight_layout() # Adding padding # xlims = ax.get_xlim() @@ -1169,10 +1173,10 @@ def ccinet_plot( def cci_map( adata: AnnData, use_label: str, - lr: str = None, - ax: matplotlib.figure.Axes = None, + lr: str | None = None, + ax: plt_axis.Axes | None = None, show: bool = False, - figsize: tuple = None, + figsize: tuple | None = None, cmap: str = "Spectral_r", sig_interactions: bool = True, title=None, @@ -1226,7 +1230,7 @@ def cci_map( # Reformat the interaction df # flat_df = create_flat_df(int_df) - ax = _box_map( + new_ax: plt_axis.Axes = _box_map( flat_df["x"], flat_df["y"], flat_df["value"].astype(int), @@ -1235,24 +1239,24 @@ def cci_map( cmap=cmap, ) - ax.set_ylabel("Sender") - ax.set_xlabel("Receiver") - plt.suptitle(title) + new_ax.set_ylabel("Sender") + new_ax.set_xlabel("Receiver") + plt.pyplot.suptitle(title) if show: - plt.show() + plt.pyplot.show() else: - return ax + return new_ax def lr_cci_map( adata: AnnData, use_label: str, - lrs: list or np.array = None, + lrs: Optional[list | np.ndarray] = None, n_top_lrs: int = 5, n_top_ccis: int = 15, min_total: int = 0, - ax: matplotlib.figure.Axes = None, + ax: plt_axis.Axes | None = None, figsize: tuple = (6.48, 4.8), show: bool = False, cmap: str = "Spectral_r", @@ -1270,8 +1274,8 @@ def lr_cci_map( Indicates the cell type labels or deconvolution results used for the cell-cell interaction counting by LR pairs. lrs: list-like - LR pairs to show in the heatmap, if None then top 5 lrs with highest no. - of interactions used from adata.uns['lr_summary']. + LR pairs to show in the heatmap, if None then top 5 lrs with the highest no. of interactions used from + adata.uns['lr_summary']. n_top_lrs: int Indicates how many top lrs to show; is ignored if lrs is not None. n_top_ccis: int @@ -1344,7 +1348,7 @@ def lr_cci_map( if flat_df.shape[0] == 0 or flat_df.shape[1] == 0: raise Exception(f"No interactions greater than min: {min_total}") - ax = _box_map( + new_ax: plt_axis.Axes = _box_map( flat_df["x"], flat_df["y"], flat_df["value"].astype(int), @@ -1354,26 +1358,26 @@ def lr_cci_map( square_scaler=square_scaler, ) - ax.set_ylabel("LR-pair") - ax.set_xlabel("Cell-cell interaction") + new_ax.set_ylabel("LR-pair") + new_ax.set_xlabel("Cell-cell interaction") if show: - plt.show() + plt.pyplot.show() else: - return ax + return new_ax def lr_chord_plot( adata: AnnData, use_label: str, - lr: str = None, + lr: str | None = None, min_ints: int = 2, n_top_ccis: int = 10, cmap: str = "default", sig_interactions: bool = True, label_size: int = 10, label_rotation: float = 0, - title: str = None, + title: str = "", figsize: tuple = (8, 8), show: bool = True, ): @@ -1435,7 +1439,7 @@ def lr_chord_plot( int_df, title = get_int_df(adata, lr, use_label, sig_interactions, title) int_df = int_df.transpose() - fig = plt.figure(figsize=figsize) + fig = plt.pyplot.figure(figsize=figsize) flux = int_df.values total_ints = flux.sum(axis=1) + flux.sum(axis=0) - flux.diagonal() @@ -1473,10 +1477,10 @@ def lr_chord_plot( # Retrieving colors of cell types # colors = get_colors(adata, use_label, cmap=cmap, label_set=cell_names) - ax = plt.axes([0, 0, 1, 1]) + ax = plt.pyplot.axes((0, 0, 1, 1)) nodePos = chordDiagram(flux, ax, lim=1.25, colors=colors) ax.axis("off") - prop = dict(fontsize=label_size, ha="center", va="center") + prop: dict[str, Any] = dict(fontsize=label_size, ha="center", va="center") label_rotation_ = label_rotation for i in range(len(cell_names)): x, y = nodePos[i][0:2] @@ -1493,14 +1497,14 @@ def lr_chord_plot( ) # size=10, fig.suptitle(title, fontsize=12, fontweight="bold") if show: - plt.show() + plt.pyplot.show() else: return fig, ax def grid_plot( adata, - use_label: str = None, + use_label: str | None = None, n_row: int = 10, n_col: int = 10, size: int = 1, @@ -1534,7 +1538,7 @@ def grid_plot( xmin, xmax = min(xedges), max(xedges) ymin, ymax = min(yedges), max(yedges) - fig, ax = plt.subplots(figsize=figsize) + fig, ax = plt.pyplot.subplots(figsize=figsize) # Plotting the points # if use_label is not None: @@ -1552,7 +1556,7 @@ def grid_plot( ax.hlines(-yedges, xmin, xmax, color="#36454F") if show: - plt.show() + plt.pyplot.show() else: return fig, ax @@ -1583,7 +1587,6 @@ def spatialcci_plot_interactive(adata: AnnData): output_notebook() show(bokeh_object.app, notebook_handle=True) - # def het_plot_interactive(adata: AnnData): # bokeh_object = BokehCciPlot(adata) # output_notebook() From 03fb048e9b4a3de21027e4ed8e89d044dad8b98a Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 4 Jun 2025 11:03:01 +1000 Subject: [PATCH 028/241] Fix names. --- stlearn/plotting/cci_plot.py | 41 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 3355d548..3a2a8bb7 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -1173,10 +1173,10 @@ def ccinet_plot( def cci_map( adata: AnnData, use_label: str, - lr: str | None = None, - ax: plt_axis.Axes | None = None, + lr_or_none: str | None = None, + ax_or_none: plt_axis.Axes | None = None, show: bool = False, - figsize: tuple | None = None, + figsize_or_none: tuple | None = None, cmap: str = "Spectral_r", sig_interactions: bool = True, title=None, @@ -1190,14 +1190,14 @@ def cci_map( use_label: str Indicates the cell type labels or deconvolution results used for cell-cell interaction counting by LR pairs. - lr: str + lr_or_none: str The LR pair to visualise the sender->receiver interactions for. If None, will use all pairs via adata.uns[f'lr_cci_{use_label}']. - ax: Axes + ax_or_none: Axes Axes on which to plot the heatmap, if None then generates own. show: bool Whether to show the plot or not; if not, then returns ax. - figsize: tuple + figsize_or_none: tuple (width, height), specifies the dimensions of the figure. Only relevant if ax=None. cmap: str @@ -1215,9 +1215,10 @@ def cci_map( """ # Either plotting overall interactions, or just for a particular LR # - int_df, title = get_int_df(adata, lr, use_label, sig_interactions, title) + int_df, title = get_int_df(adata, lr_or_none, use_label, sig_interactions, title) - if figsize is None: # Adjust size depending on no. cell types + figsize: tuple = figsize_or_none + if figsize_or_none is None: # Adjust size depending on no. cell types add = np.array([int_df.shape[0] * 0.1, int_df.shape[0] * 0.05]) figsize = tuple(np.array([6.4, 4.8]) + add) @@ -1230,23 +1231,23 @@ def cci_map( # Reformat the interaction df # flat_df = create_flat_df(int_df) - new_ax: plt_axis.Axes = _box_map( + ax: plt_axis.Axes = _box_map( flat_df["x"], flat_df["y"], flat_df["value"].astype(int), - ax=ax, + ax=ax_or_none, figsize=figsize, cmap=cmap, ) - new_ax.set_ylabel("Sender") - new_ax.set_xlabel("Receiver") + ax.set_ylabel("Sender") + ax.set_xlabel("Receiver") plt.pyplot.suptitle(title) if show: plt.pyplot.show() else: - return new_ax + return ax def lr_cci_map( @@ -1256,7 +1257,7 @@ def lr_cci_map( n_top_lrs: int = 5, n_top_ccis: int = 15, min_total: int = 0, - ax: plt_axis.Axes | None = None, + ax_or_none: plt_axis.Axes | None = None, figsize: tuple = (6.48, 4.8), show: bool = False, cmap: str = "Spectral_r", @@ -1282,7 +1283,7 @@ def lr_cci_map( Indicates maximum no. of CCIs to show. min_total: int Minimum no. of totals interaction celltypes must have to be shown. - ax: Axes + ax_or_none: Axes Axes on which to draw the heatmap, is generated internally if None. figsize: tuple (width, height), only relevant if ax=None. @@ -1348,23 +1349,23 @@ def lr_cci_map( if flat_df.shape[0] == 0 or flat_df.shape[1] == 0: raise Exception(f"No interactions greater than min: {min_total}") - new_ax: plt_axis.Axes = _box_map( + ax: plt_axis.Axes = _box_map( flat_df["x"], flat_df["y"], flat_df["value"].astype(int), - ax=ax, + ax=ax_or_none, cmap=cmap, figsize=figsize, square_scaler=square_scaler, ) - new_ax.set_ylabel("LR-pair") - new_ax.set_xlabel("Cell-cell interaction") + ax.set_ylabel("LR-pair") + ax.set_xlabel("Cell-cell interaction") if show: plt.pyplot.show() else: - return new_ax + return ax def lr_chord_plot( From 7ae45805327881046309640b6bdb9b8ef11dc22b Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 4 Jun 2025 14:51:14 +1000 Subject: [PATCH 029/241] Fix types. --- stlearn/add.py | 23 +++++++ stlearn/adds/add_image.py | 2 +- stlearn/adds/parsing.py | 2 +- stlearn/plotting/cci_plot.py | 87 +++++++++++--------------- stlearn/plotting/cci_plot_helpers.py | 6 +- stlearn/plotting/classes.py | 16 ++--- stlearn/spatials/SME/impute.py | 12 ++-- stlearn/wrapper/read.py | 91 ++++++++++++++-------------- 8 files changed, 127 insertions(+), 112 deletions(-) diff --git a/stlearn/add.py b/stlearn/add.py index e69de29b..6fd5653d 100644 --- a/stlearn/add.py +++ b/stlearn/add.py @@ -0,0 +1,23 @@ +from .adds.add_image import image +from .adds.add_positions import positions +from .adds.parsing import parsing +from .adds.add_lr import lr +from .adds.annotation import annotation +from .adds.add_labels import labels +from .adds.add_deconvolution import add_deconvolution +from .adds.add_mask import add_mask +from .adds.add_mask import apply_mask +from .adds.add_loupe_clusters import add_loupe_clusters + +__all__ = [ + "image", + "positions", + "parsing", + "lr", + "annotation", + "labels", + "add_deconvolution", + "add_mask", + "apply_mask", + "add_loupe_clusters", +] \ No newline at end of file diff --git a/stlearn/adds/add_image.py b/stlearn/adds/add_image.py index 15a4953b..0d07b3d3 100644 --- a/stlearn/adds/add_image.py +++ b/stlearn/adds/add_image.py @@ -10,7 +10,7 @@ def image( adata: AnnData, - imgpath: Path | str, + imgpath: Path | str | None, library_id: str, quality: str = "hires", scale: float = 1.0, diff --git a/stlearn/adds/parsing.py b/stlearn/adds/parsing.py index e0b2daa5..282ed38d 100644 --- a/stlearn/adds/parsing.py +++ b/stlearn/adds/parsing.py @@ -6,7 +6,7 @@ def parsing( adata: AnnData, - coordinates_file: Path | str, + coordinates_file: Path | str | None, copy: bool = True, ) -> AnnData | None: """\ diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 3a2a8bb7..f9150fee 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -45,12 +45,12 @@ def lr_diagnostics( adata, - highlight_lrs: list = None, - n_top: int = None, + highlight_lrs: list | None = None, + n_top: int | None = None, color0: str = "turquoise", color1: str = "plum", figsize: tuple = (10, 4), - lr_text_fp: dict = None, + lr_text_fp: dict | None = None, show: bool = True, ): """Diagnostic plot looking at relationship between technical features of lrs and @@ -111,14 +111,14 @@ def lr_diagnostics( def lr_summary( adata, n_top: int = 50, - highlight_lrs: list = None, + highlight_lrs: list | None = None, y: str = "n_spots_sig", color: str = "gold", - figsize: tuple = None, + figsize: tuple | None = None, highlight_color: str = "red", max_text: int = 50, - lr_text_fp: dict = None, - ax: plt_axis.Axes = None, + lr_text_fp: dict | None = None, + ax: plt_axis.Axes | None = None, show: bool = True, ): """Plotting the top LRs ranked by number of significant spots. @@ -179,8 +179,8 @@ def lr_summary( def lr_n_spots( adata, n_top: int = 100, - font_dict: dict = None, - xtick_dict: dict = None, + font_dict: dict | None = None, + xtick_dict: dict | None = None, bar_width: float = 1, max_text: int = 50, non_sig_color: str = "dodgerblue", @@ -261,10 +261,10 @@ def lr_n_spots( def lr_go( adata, n_top: int = 20, - highlight_go: list = None, + highlight_go: list | None = None, figsize=(6, 4), rot: float = 50, - lr_text_fp: dict = None, + lr_text_fp: dict | None = None, highlight_color: str = "yellow", max_text: int = 50, show: bool = True, @@ -361,12 +361,11 @@ def cci_check( xs = np.array(list(range(len(label_set)))) int_dfs = adata.uns[f"per_lr_cci_{use_label}"] - # Counting!!! # - cell_counts = [] # Cell type frequencies - cell_sigs = [] # Cell type significant interactions + cell_counts: np.ndarray = np.zeros(len(label_set), dtype=int) + cell_sigs: np.ndarray = np.zeros(len(label_set), dtype=int) for j, label in enumerate(label_set): counts = sum(labels == label) - cell_counts.append(counts) + cell_counts[j] = counts int_count = 0 for lr in int_dfs: @@ -378,11 +377,9 @@ def cci_check( # prevent double counts int_count -= int_bool[label_index, label_index] - cell_sigs.append(int_count) + cell_sigs[j] = int_count - cell_counts = np.array(cell_counts) - cell_sigs = np.array(cell_sigs) - order = np.argsort(cell_counts) + order: np.ndarray = np.argsort(cell_counts) cell_counts = cell_counts[order] cell_sigs = cell_sigs[order] colors = np.array(colors)[order] @@ -448,8 +445,8 @@ def lr_result_plot( dpi: int | None = 120, contour: bool = False, step_size: int | None = None, - vmin: float = None, - vmax: float = None, + vmin: float | None = None, + vmax: float | None = None, ): """Plots the per spot statistics for given LR. @@ -544,7 +541,7 @@ def lr_plot( lr: str, min_expr: float = 0, sig_spots=True, - use_label: str = None, + use_label: str | None = None, outer_mode: str = "continuous", l_cmap=None, r_cmap=None, @@ -557,19 +554,19 @@ def lr_plot( title="", show_image: bool = True, show_arrows: bool = False, - fig: plt_figure.Figure = None, - ax: plt_axis.Axes = None, + fig_or_none: plt_figure.Figure | None = None, + ax_or_none: plt_axis.Axes | None = None, arrow_head_width: float = 4, arrow_width: float = 0.001, - arrow_cmap: str = None, - arrow_vmax: float = None, + arrow_cmap: str | None = None, + arrow_vmax: float | None = None, sig_cci: bool = False, - lr_colors: dict = None, + lr_colors: dict | None = None, figsize: tuple = (6.4, 4.8), - use_mix: bool = None, + use_mix: bool | None = None, # plotting params **kwargs, -) -> AnnData | None: +) -> None: """Creates different kinds of spatial visualisations for the LR analysis results. To see combinations of parameters refer to stLearn CCI tutorial. @@ -621,9 +618,9 @@ def lr_plot( Whether to show the background image. show_arrows: bool Whether to plot arrows indicating interactions between spots. - fig: Figure + fig_or_none: Figure Figure to draw on. - ax: Axes + ax_or_none: Axes Axes to draw on. arrow_head_width: float Width of arrow head; only if show_arrows is true. @@ -735,8 +732,8 @@ def lr_plot( adata_full = adata # Dealing with the axis # - if fig is None or ax is None: - fig, ax = plt.subplots(figsize=figsize) + if fig_or_none is None or ax_or_none is None: + fig, ax = plt.pyplot.subplots(figsize=figsize) expr = adata.to_df() l_expr = expr.loc[:, ligand].values @@ -885,18 +882,6 @@ def lr_plot( arrow_cmap, arrow_vmax, ) - - # Cropping # - # if crop: - # x0, x1 = ax.get_xlim() - # y0, y1 = ax.get_ylim() - # x_margin, y_margin = (x1-x0)*margin_ratio, (y1-y0)*margin_ratio - # print(x_margin, y_margin) - # print(x0, x1, y0, y1) - # ax.set_xlim(x0 - x_margin, x1 + x_margin) - # ax.set_ylim(y0 - y_margin, y1 + y_margin) - # #ax.set_ylim(ax.get_ylim()[::-1]) - fig.suptitle(title) @@ -919,7 +904,7 @@ def het_plot( show_color_bar: bool | None = True, zoom_coord: float | None = None, crop: bool | None = True, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 7, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, @@ -930,9 +915,9 @@ def het_plot( use_het: str | None = "het", contour: bool = False, step_size: int | None = None, - vmin: float = None, - vmax: float = None, -) -> AnnData | None: + vmin: float | None = None, + vmax: float | None = None, +) -> None: """\ Allows the visualization of significant cell-cell interaction as the values of dot points or contour in the Spatial @@ -1217,10 +1202,12 @@ def cci_map( # Either plotting overall interactions, or just for a particular LR # int_df, title = get_int_df(adata, lr_or_none, use_label, sig_interactions, title) - figsize: tuple = figsize_or_none + figsize: tuple if figsize_or_none is None: # Adjust size depending on no. cell types add = np.array([int_df.shape[0] * 0.1, int_df.shape[0] * 0.05]) figsize = tuple(np.array([6.4, 4.8]) + add) + else: + figsize = figsize_or_none # Rank by total interactions # int_vals = int_df.values diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index 9fca0baa..f046e973 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -32,7 +32,7 @@ def lr_scatter( show=True, max_text=100, highlight_color="red", - figsize: tuple = None, + figsize: tuple | None = None, show_all: bool = False, ): """General plotting of the LR features.""" @@ -227,8 +227,8 @@ def add_arrows( sig_bool: np.array, fig, ax: Axes, - use_label: str, - int_df: pd.DataFrame, + use_label: str | None, + int_df: pd.DataFrame | None, head_width=4, width=0.001, arrow_cmap=None, diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 14efdcc2..59c38726 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -42,7 +42,7 @@ def __init__( color_bar_label: str | None = "", zoom_coord: float | None = None, crop: bool | None = True, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 7, image_alpha: float | None = 1.0, cell_alpha: float | None = 0.7, @@ -243,7 +243,7 @@ def __init__( color_bar_label: str | None = "", crop: bool | None = True, zoom_coord: float | None = None, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 7, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, @@ -1124,7 +1124,7 @@ def __init__( show_color_bar: bool | None = True, crop: bool | None = True, zoom_coord: float | None = None, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 7, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, @@ -1135,8 +1135,8 @@ def __init__( use_het: str | None = "het", contour: bool = False, step_size: int | None = None, - vmin: float = None, - vmax: float = None, + vmin: float | None = None, + vmax: float | None = None, **kwargs, ): super().__init__( @@ -1194,7 +1194,7 @@ def __init__( show_color_bar: bool | None = True, crop: bool | None = True, zoom_coord: float | None = None, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 7, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, @@ -1204,8 +1204,8 @@ def __init__( # cci_rank param contour: bool = False, step_size: int | None = None, - vmin: float = None, - vmax: float = None, + vmin: float | None = None, + vmax: float | None = None, **kwargs, ): # Making sure cci_rank has been run first # diff --git a/stlearn/spatials/SME/impute.py b/stlearn/spatials/SME/impute.py index 31f16467..a365b192 100644 --- a/stlearn/spatials/SME/impute.py +++ b/stlearn/spatials/SME/impute.py @@ -90,7 +90,7 @@ def pseudo_spot( adata: AnnData, tile_path: Path | str = Path("/tmp/tiles"), use_data: str = "raw", - crop_size: int = "auto", + crop_size: str | int = "auto", platform: _PLATFORM = "Visium", weights: _WEIGHTING_MATRIX = "weights_matrix_all", copy: _COPY = "pseudo_spot_adata", @@ -279,10 +279,14 @@ def pseudo_spot( pseudo_spot_adata = AnnData(impute_df, obs=obs_df) pseudo_spot_adata.uns["spatial"] = adata.uns["spatial"] + actual_crop_size: int if crop_size == "auto": - crop_size = round(unit / 2) - - stlearn.pp.tiling(pseudo_spot_adata, tile_path, crop_size=crop_size) + actual_crop_size = round(unit / 2) + elif isinstance(crop_size, int): + actual_crop_size = crop_size + else: + raise ValueError(f"crop_size must be 'auto' or an integer, got {crop_size}") + stlearn.pp.tiling(pseudo_spot_adata, tile_path, crop_size=actual_crop_size) stlearn.pp.extract_feature(pseudo_spot_adata) diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 030e7d82..10118892 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -16,18 +16,18 @@ from .._compat import Literal -_QUALITY = Literal["fulres", "hires", "lowres"] -_background = ["black", "white"] +_Quality = Literal["fulres", "hires", "lowres"] +_Background = Literal["black", "white"] def Read10X( path: str | Path, genome: str | None = None, count_file: str = "filtered_feature_bc_matrix.h5", - library_id: str = None, - load_images: bool | None = True, - quality: _QUALITY = "hires", - image_path: str | Path = None, + library_id: str | None = None, + load_images: bool = True, + quality: _Quality = "hires", + image_path: str | Path | None = None, ) -> AnnData: """\ Read Visium data from 10X (wrap read_visium from scanpy) @@ -96,9 +96,9 @@ def Read10X( with File(path / count_file, mode="r") as f: attrs = dict(f.attrs) + if library_id is None: library_id = str(attrs.pop("library_ids")[0], "utf-8") - adata.uns["spatial"][library_id] = dict() tissue_positions_file = ( @@ -170,32 +170,33 @@ def Read10X( inplace=True, ) - # put image path in uns - if image_path is not None: - # get an absolute path - image_path = str(Path(image_path).resolve()) - adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( - image_path - ) - - adata.var_names_make_unique() + if quality == "fulres": + # put image path in uns + if image_path is not None: + # get an absolute path + image_path = str(Path(image_path).resolve()) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( + image_path + ) + else: + raise ValueError( + "Trying to load fulres but no image_path set." + ) - if library_id is None: - library_id = list(adata.uns["spatial"].keys())[0] + image_coor = adata.obsm["spatial"] + img = plt.imread(image_path, None) + adata.uns["spatial"][library_id]["images"]["fulres"] = img + else: + scale = adata.uns["spatial"][library_id]["scalefactors"][ + "tissue_" + quality + "_scalef" + ] + image_coor = adata.obsm["spatial"] * scale - if quality == "fulres": - image_coor = adata.obsm["spatial"] - img = plt.imread(image_path, 0) - adata.uns["spatial"][library_id]["images"]["fulres"] = img - else: - scale = adata.uns["spatial"][library_id]["scalefactors"][ - "tissue_" + quality + "_scalef" - ] - image_coor = adata.obsm["spatial"] * scale + adata.obs["imagecol"] = image_coor[:, 0] + adata.obs["imagerow"] = image_coor[:, 1] + adata.uns["spatial"][library_id]["use_quality"] = quality - adata.obs["imagecol"] = image_coor[:, 0] - adata.obs["imagerow"] = image_coor[:, 1] - adata.uns["spatial"][library_id]["use_quality"] = quality + adata.var_names_make_unique() adata.obs["array_row"] = adata.obs["array_row"].astype(int) adata.obs["array_col"] = adata.obs["array_col"].astype(int) @@ -205,9 +206,9 @@ def Read10X( def ReadOldST( - count_matrix_file: str | Path = None, - spatial_file: str | Path = None, - image_file: str | Path = None, + count_matrix_file: str | Path | None = None, + spatial_file: str | Path | None = None, + image_file: str | Path | None = None, library_id: str = "OldST", scale: float = 1.0, quality: str = "hires", @@ -257,11 +258,11 @@ def ReadOldST( def ReadSlideSeq( count_matrix_file: str | Path, spatial_file: str | Path, - library_id: str = None, - scale: float = None, + library_id: str| None = None, + scale: float | None = None, quality: str = "hires", spot_diameter_fullres: float = 50, - background_color: _background = "white", + background_color: _Background = "white", ) -> AnnData: """\ Read Slide-seq data @@ -340,11 +341,11 @@ def ReadSlideSeq( def ReadMERFISH( count_matrix_file: str | Path, spatial_file: str | Path, - library_id: str = None, - scale: float = None, + library_id: str | None = None, + scale: float | None = None, quality: str = "hires", spot_diameter_fullres: float = 50, - background_color: _background = "white", + background_color: _Background = "white", ) -> AnnData: """\ Read MERFISH data @@ -423,12 +424,12 @@ def ReadMERFISH( def ReadSeqFish( count_matrix_file: str | Path, spatial_file: str | Path, - library_id: str = None, + library_id: str | None = None, scale: float = 1.0, quality: str = "hires", field: int = 0, spot_diameter_fullres: float = 50, - background_color: _background = "white", + background_color: _Background = "white", ) -> AnnData: """\ Read SeqFish data @@ -512,11 +513,11 @@ def ReadXenium( feature_cell_matrix_file: str | Path, cell_summary_file: str | Path, image_path: Path | None = None, - library_id: str = None, + library_id: str | None = None, scale: float = 1.0, quality: str = "hires", spot_diameter_fullres: float = 15, - background_color: _background = "white", + background_color: _Background = "white", ) -> AnnData: """\ Read Xenium data @@ -606,10 +607,10 @@ def create_stlearn( spatial: pd.DataFrame, library_id: str, image_path: Path | None = None, - scale: float = None, + scale: float | None = None, quality: str = "hires", spot_diameter_fullres: float = 50, - background_color: _background = "white", + background_color: _Background = "white", ): """\ Create AnnData object for stLearn From 13f0074edf3b16bb60b03551dc26f3f451238800 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 4 Jun 2025 14:53:03 +1000 Subject: [PATCH 030/241] Use float instead of int for reading in pxl coordinates. --- stlearn/wrapper/read.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 10118892..ee44242d 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -163,7 +163,7 @@ def Read10X( adata.obsm["spatial"] = ( adata.obs[["pxl_row_in_fullres", "pxl_col_in_fullres"]] .to_numpy() - .astype(int) + .astype(float) ) adata.obs.drop( columns=["barcode", "pxl_row_in_fullres", "pxl_col_in_fullres"], From d9f7d8dffcb5a59ad1b2bd6039a06b23aea233ea Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 4 Jun 2025 15:42:49 +1000 Subject: [PATCH 031/241] Fix more types. --- stlearn/tools/microenv/cci/analysis.py | 41 ++++++++-------- stlearn/tools/microenv/cci/base.py | 60 +++++++++++------------ stlearn/tools/microenv/cci/het.py | 4 +- stlearn/tools/microenv/cci/perm_utils.py | 2 +- stlearn/tools/microenv/cci/permutation.py | 2 +- 5 files changed, 53 insertions(+), 56 deletions(-) diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index 16d21cad..6a22672d 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -25,7 +25,7 @@ # Functions related to Ligand-Receptor interactions -def load_lrs(names: str | list | None = None, species: str = "human") -> np.array: +def load_lrs(names: str | list | None = None, species: str = "human") -> np.ndarray: """Loads inputted LR database, & concatenates into consistent database set of pairs without duplicates. If None loads 'connectomeDB2020_lit'. @@ -53,12 +53,12 @@ def load_lrs(names: str | list | None = None, species: str = "human") -> np.arra for db in dbs: lrs = [f"{db.values[i,0]}_{db.values[i,1]}" for i in range(db.shape[0])] lrs_full.extend(lrs) - lrs_full = np.unique(lrs_full) + lrs_full_arr = np.unique(np.array(lrs_full)) # If dealing with mouse, need to reformat # if species == "mouse": genes1 = [lr_.split("_")[0] for lr_ in lrs_full] genes2 = [lr_.split("_")[1] for lr_ in lrs_full] - lrs_full = np.array( + lrs_full_arr = np.array( [ genes1[i][0] + genes1[i][1:].lower() @@ -69,14 +69,14 @@ def load_lrs(names: str | list | None = None, species: str = "human") -> np.arra ] ) - return lrs_full + return lrs_full_arr def grid( adata, n_row: int = 10, n_col: int = 10, - use_label: str = None, + use_label: str | None = None, n_cpus: int = 1, verbose: bool = True, ): @@ -165,7 +165,7 @@ def grid( grid_data.obsm["spatial"] = grid_coords grid_data.uns["spatial"] = adata.uns["spatial"] - if use_label is not None: + if use_label is not None and cell_info is not None and cell_set is not None: grid_data.uns[use_label] = pd.DataFrame( cell_info, index=grid_data.obs_names.values.astype(str), columns=cell_set ) @@ -192,12 +192,12 @@ def grid( def run( adata: AnnData, - lrs: np.array, + lrs: np.ndarray, min_spots: int = 10, - distance: int = None, + distance: int | None = None, n_pairs: int = 1000, - n_cpus: int = None, - use_label: str = None, + n_cpus: int | None = None, + use_label: str | None = None, adj_method: str = "fdr_bh", pval_adj_cutoff: float = 0.05, min_expr: float = 0, @@ -211,7 +211,7 @@ def run( ----------- adata: AnnData The data object. - lrs: np.array + lrs: np.ndarray The LR pairs to score/test for enrichment (in format 'L1_R1'). min_spots: int Minimum number of spots with an LR score for an LR to be considered for @@ -261,7 +261,7 @@ def run( referring to the LRs listed in adata.uns['lr_summary']. 'lr_scores' is the raw scores, while 'lr_sig_scores' is the same except only for significant scores; non-significant scores are set to zero. - adata.obsm['het'] + adata.obsm['cci_het'] Only if use_label specified; contains the counts of the cell types found per spot. """ @@ -317,7 +317,7 @@ def run( print("Calculating cell heterogeneity...") # Calculating cell heterogeneity # - count(adata, distance=distance, use_label=use_label, use_het=use_label) + count(adata, distance=distance, use_label=use_label) het_vals = ( np.array([1] * len(adata)) @@ -328,13 +328,13 @@ def run( """ 1. Filter any LRs without stored expression. """ # Calculating the lr_scores across spots for the inputted lrs # - lr_scores, lrs = get_lrs_scores(adata, lrs, neighbours, het_vals, min_expr) + lr_scores, new_lrs = get_lrs_scores(adata, lrs, neighbours, het_vals, min_expr) lr_bool = (lr_scores > 0).sum(axis=0) > min_spots - lrs = lrs[lr_bool] + new_lrs = new_lrs[lr_bool] lr_scores = lr_scores[:, lr_bool] if verbose: - print("Altogether " + str(len(lrs)) + " valid L-R pairs") - if len(lrs) == 0: + print("Altogether " + str(len(new_lrs)) + " valid L-R pairs") + if len(new_lrs) == 0: print("Exiting due to lack of valid LR pairs.") return @@ -343,7 +343,7 @@ def run( perform_spot_testing( adata, lr_scores, - lrs, + new_lrs, n_pairs, neighbours, het_vals, @@ -442,7 +442,7 @@ def run_lr_go( adata: AnnData, r_path: str, n_top: int = 100, - bg_genes: np.array = None, + bg_genes: np.ndarray | None = None, min_sig_spots: int = 1, species: str = "human", p_cutoff: float = 0.01, @@ -497,7 +497,8 @@ def run_lr_go( # Determining the background genes if not inputted if bg_genes is None: all_lrs = load_lrs("connectomeDB2020_put") - bg_genes = np.unique([lr_.split("_") for lr_ in all_lrs]) + all_genes = [lr_.split("_") for lr_ in all_lrs] + bg_genes = np.unique(all_genes) # Running the GO analysis go_results = run_GO( diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tools/microenv/cci/base.py index edbc5c66..2e852d3a 100644 --- a/stlearn/tools/microenv/cci/base.py +++ b/stlearn/tools/microenv/cci/base.py @@ -71,7 +71,7 @@ def lr( # return adata -def calc_distance(adata: AnnData, distance: float): +def calc_distance(adata: AnnData, distance: float | None): """Automatically calculate distance if not given, won't overwrite \ distance=0 which is within-spot. Parameters @@ -95,7 +95,7 @@ def calc_distance(adata: AnnData, distance: float): scalefactors["spot_diameter_fullres"] * scalefactors[ "tissue_" + adata.uns["spatial"][library_id]["use_quality"] + "_scalef" - ] + ] * 2 ) return distance @@ -103,34 +103,34 @@ def calc_distance(adata: AnnData, distance: float): def get_lrs_scores( adata: AnnData, - lrs: np.array, - neighbours: np.array, - het_vals: np.array, + lrs: np.ndarray, + neighbours: np.ndarray, + het_vals: np.ndarray, min_expr: float, filter_pairs: bool = True, - spot_indices: np.array = None, + spot_indices: np.ndarray | None = None, ): """Gets the scores for the indicated set of LR pairs & the heterogeneity values. Parameters ---------- adata: AnnData See run() doc-string. - lrs: np.array + lrs: np.ndarray See run() doc-string. - neighbours: np.array + neighbours: np.ndarray Array of arrays with indices specifying neighbours of each spot. - het_vals: np.array + het_vals: np.ndarray Cell heterogeneity counts per spot. min_expr: float Minimum gene expression of either L or R for spot to be considered to have reasonable score. filter_pairs: bool Whether to filter to valid pairs or not. - spot_indices: np.array + spot_indices: np.ndarray Array of integers speci Returns ------- - lrs: np.array lr pairs from the database in format ['L1_R1', 'LN_RN'] + lrs: np.ndarray lr pairs from the database in format ['L1_R1', 'LN_RN'] """ if spot_indices is None: spot_indices = np.array(list(range(len(adata))), dtype=np.int32) @@ -141,28 +141,24 @@ def get_lrs_scores( spot_lr2s = get_spot_lrs( adata, lr_pairs=lrs, lr_order=False, filter_pairs=filter_pairs ) - if filter_pairs: - lrs = np.array( - [ - "_".join(spot_lr1s.columns.values[i : i + 2]) - for i in range(0, spot_lr1s.shape[1], 2) - ] - ) - # Calculating the lr_scores across spots for the inputted lrs # lr_scores = get_scores( spot_lr1s.values, spot_lr2s.values, neighbours, het_vals, min_expr, spot_indices ) - if filter_pairs: - return lr_scores, lrs - else: - return lr_scores + new_lrs = np.array( + [ + "_".join(spot_lr1s.columns.values[i: i + 2]) + for i in range(0, spot_lr1s.shape[1], 2) + ] + ) + + return lr_scores, new_lrs def get_spot_lrs( adata: AnnData, - lr_pairs: list, + lr_pairs: np.ndarray, lr_order: bool, filter_pairs: bool = True, ): @@ -171,8 +167,8 @@ def get_spot_lrs( ---------- adata (AnnData): The adata object to scan - lr_pairs (list): - List of the lr pairs (e.g. ['L1_R1', 'L2_R2',...] + lr_pairs (np.ndarray): + np.ndarray of the lr pairs (e.g. ['L1_R1', 'L2_R2',...] lr_order (bool): Forward version of the spot lr pairs (L1_R1), False indicates reverse (R1_L1) filter_pairs (bool): @@ -204,7 +200,7 @@ def get_spot_lrs( def calc_neighbours( adata: AnnData, - distance: float = None, + distance: float | None = None, index: bool = True, verbose: bool = True, ) -> List: @@ -365,10 +361,10 @@ def get_scores( spot_lr1s: np.ndarray, spot_lr2s: np.ndarray, neighbours: List, - het_vals: np.array, + het_vals: np.ndarray, min_expr: float, - spot_indices: np.array, -) -> np.array: + spot_indices: np.ndarray, +) -> np.ndarray: """Calculates the scores. Parameters ---------- @@ -390,7 +386,7 @@ def get_scores( spot_scores = np.zeros((len(spot_indices), spot_lr1s.shape[1] // 2), np.float64) for i in prange(0, spot_lr1s.shape[1] // 2): i_ = i * 2 # equivalent to range(0, spot_lr1s.shape[1], 2) - spot_lr1, spot_lr2 = spot_lr1s[:, i_ : (i_ + 2)], spot_lr2s[:, i_ : (i_ + 2)] + spot_lr1, spot_lr2 = spot_lr1s[:, i_: (i_ + 2)], spot_lr2s[:, i_: (i_ + 2)] lr_scores = lr_core(spot_lr1, spot_lr2, neighbours, min_expr, spot_indices) # The merge scores # lr_scores = np.multiply(het_vals[spot_indices], lr_scores) @@ -450,7 +446,7 @@ def lr_grid( & (coor["imagecol"] < grid[0] + width) & (coor["imagerow"] < grid[1]) & (coor["imagerow"] > grid[1] - height) - ] + ] df_grid.loc[n] = df.loc[spots.index].sum() # expand the LR pairs list by swapping ligand-receptor positions diff --git a/stlearn/tools/microenv/cci/het.py b/stlearn/tools/microenv/cci/het.py index 7b3a7bdf..54232564 100644 --- a/stlearn/tools/microenv/cci/het.py +++ b/stlearn/tools/microenv/cci/het.py @@ -17,10 +17,10 @@ def count( adata: AnnData, - use_label: str = None, + use_label: str | None = None, use_het: str = "cci_het", verbose: bool = True, - distance: float = None, + distance: float | None = None, ) -> AnnData: """Count the cell type densities Parameters diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index b7d9ab61..4e147b07 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -372,7 +372,7 @@ def get_lr_bg( rand_pairs = gen_rand_pairs(l_genes, r_genes, n_pairs) spot_indices = np.where(lr_score > 0)[0] - background = get_lrs_scores( + background, _ = get_lrs_scores( adata, rand_pairs, neighbours, diff --git a/stlearn/tools/microenv/cci/permutation.py b/stlearn/tools/microenv/cci/permutation.py index 79b118c3..267b224c 100644 --- a/stlearn/tools/microenv/cci/permutation.py +++ b/stlearn/tools/microenv/cci/permutation.py @@ -273,7 +273,7 @@ def perform_perm_testing( # Calculating the background # rand_pairs = get_rand_pairs(adata, genes, n_pairs, lrs=lrs, im=group_im) - background = get_lrs_scores( + background, _ = get_lrs_scores( adata, rand_pairs, neighbours, From 353a6686088ab532d84a67807dcbcdd1bc4d7a84 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 08:27:39 +1000 Subject: [PATCH 032/241] Fixing small mistakes in formatting and doco. --- CONTRIBUTING.rst | 2 +- stlearn/add.py | 15 +++++++-------- stlearn/plotting/cci_plot.py | 17 ++++++++--------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 40cc4228..f6f1d45e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -87,7 +87,7 @@ Ready to contribute? Here's how to set up `stlearn` for local development. 5. When you're done making changes, check that your changes pass linters and tests:: $ black stlearn tests - $ flake8 stlearn tests + $ ruff check stlearn tests $ mypy stlearn tests $ pytest diff --git a/stlearn/add.py b/stlearn/add.py index 6fd5653d..025a232a 100644 --- a/stlearn/add.py +++ b/stlearn/add.py @@ -1,13 +1,12 @@ +from .adds.add_deconvolution import add_deconvolution from .adds.add_image import image -from .adds.add_positions import positions -from .adds.parsing import parsing -from .adds.add_lr import lr -from .adds.annotation import annotation from .adds.add_labels import labels -from .adds.add_deconvolution import add_deconvolution -from .adds.add_mask import add_mask -from .adds.add_mask import apply_mask from .adds.add_loupe_clusters import add_loupe_clusters +from .adds.add_lr import lr +from .adds.add_mask import add_mask, apply_mask +from .adds.add_positions import positions +from .adds.annotation import annotation +from .adds.parsing import parsing __all__ = [ "image", @@ -20,4 +19,4 @@ "add_mask", "apply_mask", "add_loupe_clusters", -] \ No newline at end of file +] diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index f9150fee..82723338 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -2,22 +2,21 @@ import math import sys from typing import ( - Optional, Any, # Special + Any, + Optional, # Special ) import matplotlib +import matplotlib as plt +import matplotlib.axes as plt_axis +import matplotlib.figure as plt_figure import matplotlib.patches as patches import networkx as nx import numpy as np import pandas as pd -import matplotlib as plt -import matplotlib.axes as plt_axis -import matplotlib.figure as plt_figure from anndata import AnnData from bokeh.io import output_notebook from bokeh.plotting import show -from numpy.typing import NDArray - from scipy.stats import gaussian_kde import stlearn.plotting.cci_plot_helpers as cci_hs @@ -1240,7 +1239,7 @@ def cci_map( def lr_cci_map( adata: AnnData, use_label: str, - lrs: Optional[list | np.ndarray] = None, + lrs: list | np.ndarray | None = None, n_top_lrs: int = 5, n_top_ccis: int = 15, min_total: int = 0, @@ -1262,8 +1261,8 @@ def lr_cci_map( Indicates the cell type labels or deconvolution results used for the cell-cell interaction counting by LR pairs. lrs: list-like - LR pairs to show in the heatmap, if None then top 5 lrs with the highest no. of interactions used from - adata.uns['lr_summary']. + LR pairs to show in the heatmap, if None then top 5 lrs with the highest + no. of interactions used from adata.uns['lr_summary']. n_top_lrs: int Indicates how many top lrs to show; is ignored if lrs is not None. n_top_ccis: int From b68174cba30587706da5d20428bd9978593cd6ef Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 08:56:18 +1000 Subject: [PATCH 033/241] Misc fixes to typing and documentation. --- LICENSE | 4 +--- pyproject.toml | 10 +++++----- stlearn/adds/add_lr.py | 2 +- stlearn/logging.py | 2 +- stlearn/plotting/classes.py | 14 +++++++------- stlearn/plotting/cluster_plot.py | 2 +- stlearn/plotting/feat_plot.py | 2 +- stlearn/plotting/gene_plot.py | 2 +- stlearn/plotting/subcluster_plot.py | 2 +- 9 files changed, 19 insertions(+), 21 deletions(-) diff --git a/LICENSE b/LICENSE index 626beb6e..fafffeca 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,6 @@ - - BSD License -Copyright (c) 2020, Genomics and Machine Learning lab +Copyright (c) 2020-2025, Genomics and Machine Learning lab All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/pyproject.toml b/pyproject.toml index fda624e5..f97a190c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,11 +27,11 @@ dynamic = ["dependencies"] [project.optional-dependencies] dev = [ - "black", - "ruff", - "mypy", - "pytest", - "tox", + "black>=23.0", + "ruff>=0.1.0", + "mypy>=1.10", + "pytest>=7.0", + "tox>=4.0", ] test = [ "pytest", diff --git a/stlearn/adds/add_lr.py b/stlearn/adds/add_lr.py index d979a2ac..78df7424 100644 --- a/stlearn/adds/add_lr.py +++ b/stlearn/adds/add_lr.py @@ -4,7 +4,7 @@ def lr( adata: AnnData, - db_filepath: str = None, + db_filepath: str, sep: str = "\t", source: str = "connectomedb", copy: bool = False, diff --git a/stlearn/logging.py b/stlearn/logging.py index e23e2786..24421cc2 100644 --- a/stlearn/logging.py +++ b/stlearn/logging.py @@ -177,7 +177,7 @@ def _copy_docs_and_signature(fn): def error( msg: str, *, - time: datetime = None, + time: datetime | None = None, deep: str | None = None, extra: dict | None = None, ) -> datetime: diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 59c38726..4eb24266 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -256,8 +256,8 @@ def __init__( method: str = "CumSum", contour: bool = False, step_size: int | None = None, - vmin: float = None, - vmax: float = None, + vmin: float | None = None, + vmax: float | None = None, **kwargs, ): super().__init__( @@ -455,7 +455,7 @@ def __init__( color_bar_label: str | None = "", crop: bool | None = True, zoom_coord: float | None = None, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 7, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, @@ -467,8 +467,8 @@ def __init__( threshold: float | None = None, contour: bool = False, step_size: int | None = None, - vmin: float = None, - vmax: float = None, + vmin: float | None = None, + vmax: float | None = None, **kwargs, ): super().__init__( @@ -618,7 +618,7 @@ def __init__( show_color_bar: bool | None = True, crop: bool | None = True, zoom_coord: float | None = None, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 5, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, @@ -970,7 +970,7 @@ def __init__( show_color_bar: bool | None = True, crop: bool | None = True, zoom_coord: float | None = None, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 5, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index c5d1b08e..e2c2e0c1 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -30,7 +30,7 @@ def cluster_plot( show_color_bar: bool | None = True, zoom_coord: float | None = None, crop: bool | None = True, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 5, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index 1172cbc2..d469ab8c 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -33,7 +33,7 @@ def feat_plot( color_bar_label: str | None = "", zoom_coord: float | None = None, crop: bool | None = True, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 7, image_alpha: float | None = 1.0, cell_alpha: float | None = 0.7, diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index d4ebcdff..290d33e9 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -35,7 +35,7 @@ def gene_plot( color_bar_label: str | None = "", zoom_coord: float | None = None, crop: bool | None = True, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 7, image_alpha: float | None = 1.0, cell_alpha: float | None = 0.7, diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/plotting/subcluster_plot.py index af603e13..4dcef013 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/plotting/subcluster_plot.py @@ -26,7 +26,7 @@ def subcluster_plot( show_image: bool | None = True, show_color_bar: bool | None = True, crop: bool | None = True, - margin: bool | None = 100, + margin: float | None = 100, size: float | None = 5, image_alpha: float | None = 1.0, cell_alpha: float | None = 1.0, From 7be6dd6e76c5721701d3c8e4e6e13de66c5d99c9 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 09:17:15 +1000 Subject: [PATCH 034/241] Update numpy, tensorflow and numba. --- pyproject.toml | 2 +- requirements.txt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f97a190c..d5a137fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dynamic = ["dependencies"] dev = [ "black>=23.0", "ruff>=0.1.0", - "mypy>=1.10", + "mypy>=1.16", "pytest>=7.0", "tox>=4.0", ] diff --git a/requirements.txt b/requirements.txt index 616bcd91..7b8e4ed0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,11 +2,11 @@ bokeh==3.7.3 click==8.2.1 leidenalg==0.10.2 louvain==0.8.2 -numba==0.55.2 -numpy==1.22.4 +numba==0.56.4 +numpy==1.23.5 pillow==11.2.1 -scanpy==1.9.8 +scanpy==1.10.4 scikit-image==0.22.0 -tensorflow==2.13.1 +tensorflow==2.14.1 imageio==2.37.0 scipy==1.11.4 \ No newline at end of file From 9736e3447b4c48b43ee3f3f72b67254c2ee90e89 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 11:00:20 +1000 Subject: [PATCH 035/241] More fixes. --- mypy.ini | 2 + pyproject.toml | 14 ++ requirements.txt | 2 + stlearn/app/app.py | 6 +- stlearn/app/cli.py | 8 +- stlearn/app/source/forms/views.py | 6 +- stlearn/image_preprocessing/model_zoo.py | 12 +- stlearn/image_preprocessing/segmentation.py | 191 -------------------- stlearn/plotting/cci_plot.py | 1 + stlearn/tools/microenv/cci/base.py | 8 +- stlearn/wrapper/read.py | 6 +- 11 files changed, 41 insertions(+), 215 deletions(-) create mode 100644 mypy.ini delete mode 100644 stlearn/image_preprocessing/segmentation.py diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..01f6bb35 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +follow_untyped_imports = True \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d5a137fa..84795255 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,20 @@ test = [ "pytest", "pytest-cov", ] +webapp = [ + "flask>=2.0.0", + "flask-wtf>=1.0.0", + "wtforms>=3.0.0", + "markupsafe>2.1.0", +] +jupyter = [ + "jupyter>=1.0.0", + "jupyterlab>=3.0.0", + "ipywidgets>=7.6.0", + "plotly>=5.0.0", + "bokeh>=2.4.0", + "rpy2>=3.4.0", +] [project.urls] Homepage = "https://github.com/BiomedicalMachineLearning/stLearn" diff --git a/requirements.txt b/requirements.txt index 7b8e4ed0..a1f23831 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,5 +8,7 @@ pillow==11.2.1 scanpy==1.10.4 scikit-image==0.22.0 tensorflow==2.14.1 +keras==2.14.0 +types-tensorflow>=2.8.0 imageio==2.37.0 scipy==1.11.4 \ No newline at end of file diff --git a/stlearn/app/app.py b/stlearn/app/app.py index 8f6ccd74..7343ceeb 100644 --- a/stlearn/app/app.py +++ b/stlearn/app/app.py @@ -6,7 +6,7 @@ sys.path.append(os.path.dirname(__file__)) try: - import flask # noqa: F401 + import flask except ImportError: subprocess.call( "pip install -r " + os.path.dirname(__file__) + "//requirements.txt", shell=True @@ -34,7 +34,7 @@ ) # Functions related to processing the forms. -from source.forms import views # for changing data in response to input +from stlearn.app.source.forms import views # for changing data in response to input from tornado.ioloop import IOLoop from werkzeug.utils import secure_filename @@ -482,7 +482,7 @@ def bk_worker(): "/bokeh_annotate_plot": bkapp4, }, io_loop=IOLoop(), - allow_websocket_origin=["127.0.0.1:5000", "localhost:5000"], + allow_websocket_origin=["127.0.0.1:3000", "localhost:3000"], ) server.start() server.io_loop.start() diff --git a/stlearn/app/cli.py b/stlearn/app/cli.py index ff45c24a..42e92779 100644 --- a/stlearn/app/cli.py +++ b/stlearn/app/cli.py @@ -1,6 +1,4 @@ import errno -import os - import click from .. import __version__ @@ -20,7 +18,6 @@ help="Show the software version and exit.", ) def main(): - os._exit click.echo("Please run `stlearn launch` to start the web app") @@ -29,10 +26,13 @@ def launch(): from .app import app try: - app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False) + app.run(host="0.0.0.0", port=3000, debug=True, use_reloader=False) except OSError as e: if e.errno == errno.EADDRINUSE: raise click.ClickException( "Port is in use, please specify an open port using the --port flag." ) from e raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/stlearn/app/source/forms/views.py b/stlearn/app/source/forms/views.py index 09dc3888..30a2c672 100644 --- a/stlearn/app/source/forms/views.py +++ b/stlearn/app/source/forms/views.py @@ -9,10 +9,10 @@ import numpy import numpy as np import scanpy as sc -import source.forms.view_helpers as vhs from flask import flash, render_template -from source.forms import forms -from source.forms.utils import flash_errors +import stlearn.app.source.forms.view_helpers as vhs +from stlearn.app.source.forms import forms +from stlearn.app.source.forms.utils import flash_errors import stlearn as st diff --git a/stlearn/image_preprocessing/model_zoo.py b/stlearn/image_preprocessing/model_zoo.py index 7faf2673..c21af134 100644 --- a/stlearn/image_preprocessing/model_zoo.py +++ b/stlearn/image_preprocessing/model_zoo.py @@ -8,7 +8,7 @@ class Model: __name__ = "CNN base model" def __init__(self, base, batch_size=1): - from tensorflow.keras import backend as keras + from keras import backend as keras self.base = base self.model, self.preprocess = self.load_model() @@ -17,7 +17,7 @@ def __init__(self, base, batch_size=1): def load_model(self): if self.base == "resnet50": - from tensorflow.keras.applications.resnet50 import ( + from keras.applications.resnet50 import ( ResNet50, preprocess_input, ) @@ -26,11 +26,11 @@ def load_model(self): include_top=False, weights="imagenet", pooling="avg" ) elif self.base == "vgg16": - from tensorflow.keras.applications.vgg16 import VGG16, preprocess_input + from keras.applications.vgg16 import VGG16, preprocess_input cnn_base_model = VGG16(include_top=False, weights="imagenet", pooling="avg") elif self.base == "inception_v3": - from tensorflow.keras.applications.inception_v3 import ( + from keras.applications.inception_v3 import ( InceptionV3, preprocess_input, ) @@ -39,7 +39,7 @@ def load_model(self): include_top=False, weights="imagenet", pooling="avg" ) elif self.base == "xception": - from tensorflow.keras.applications.xception import ( + from keras.applications.xception import ( Xception, preprocess_input, ) @@ -52,7 +52,7 @@ def load_model(self): return cnn_base_model, preprocess_input def predict(self, x): - from tensorflow.keras import backend as keras + from keras import backend as keras if self.data_format == "channels_first": x = x.transpose(0, 3, 1, 2) diff --git a/stlearn/image_preprocessing/segmentation.py b/stlearn/image_preprocessing/segmentation.py deleted file mode 100644 index 6d975aab..00000000 --- a/stlearn/image_preprocessing/segmentation.py +++ /dev/null @@ -1,191 +0,0 @@ -import histomicstk as htk -import numpy as np -import scipy as sp -import skimage.color -import skimage.io -import skimage.measure -from anndata import AnnData -from scipy import ndimage as ndi -from skimage.feature import peak_local_max -from skimage.segmentation import watershed -from tqdm import tqdm - - -def morph_watershed( - adata: AnnData, - library_id: str = None, - verbose: bool = False, - copy: bool = False, -) -> AnnData | None: - """\ - Watershed method to segment nuclei and calculate morphological statistics - - Parameters - ---------- - adata - Annotated data matrix. - library_id - Library id stored in AnnData. - copy - Return a copy instead of writing to adata. - Returns - ------- - Depending on `copy`, returns or updates `adata` with the following fields. - **n_nuclei** : `adata.obs` field - saved number of nuclei of each spot image tiles - **nuclei_total_area** : `adata.obs` field - saved of total area of nuclei of each spot image tiles - **nuclei_mean_area** : `adata.obs` field - saved mean area of nuclei of each spot image tiles - **nuclei_std_area** : `adata.obs` field - saved stand deviation of nuclei area of each spot image tiles - **eccentricity** : `adata.obs` field - saved eccentricity of each spot image tiles - **mean_pix_r** : `adata.obs` field - saved mean pixel value of red channel of of each spot image tiles - **std_pix_r** : `adata.obs` field - saved stand deviation of red channel of each spot image tiles - **mean_pix_g** : `adata.obs` field - saved mean pixel value of green channel of each spot image tiles - **std_pix_g** : `adata.obs` field - saved stand deviation of green channel of each spot image tiles - **mean_pix_b** : `adata.obs` field - saved mean pixel value of blue channel of each spot image tiles - **std_pix_b** : `adata.obs` field - saved stand deviation of blue channel of each spot image tiles - **nuclei_total_area_per_tile** : `adata.obs` field - saved total nuclei area per tile of each spot image tiles - """ - - if library_id is None: - library_id = list(adata.uns["spatial"].keys())[0] - - n_nuclei_list = [] - nuclei_total_area_list = [] - nuclei_mean_area_list = [] - nuclei_std_area_list = [] - eccentricity_list = [] - mean_pix_list_r = [] - std_pix_list_r = [] - mean_pix_list_g = [] - std_pix_list_g = [] - mean_pix_list_b = [] - std_pix_list_b = [] - with tqdm( - total=len(adata), - desc="calculate morphological stats", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", - ) as pbar: - for tile in adata.obs["tile_path"]: - ( - n_nuclei, - nuclei_total_area, - nuclei_mean_area, - nuclei_std_area, - eccentricity, - solidity, - mean_pix_r, - std_pix_r, - mean_pix_g, - std_pix_g, - mean_pix_b, - std_pix_b, - ) = _calculate_morph_stats(tile) - n_nuclei_list.append(n_nuclei) - nuclei_total_area_list.append(nuclei_total_area) - nuclei_mean_area_list.append(nuclei_mean_area) - nuclei_std_area_list.append(nuclei_std_area) - eccentricity_list.append(eccentricity) - mean_pix_list_r.append(mean_pix_r) - std_pix_list_r.append(std_pix_r) - mean_pix_list_g.append(mean_pix_g) - std_pix_list_g.append(std_pix_g) - mean_pix_list_b.append(mean_pix_b) - std_pix_list_b.append(std_pix_b) - pbar.update(1) - - adata.obs["n_nuclei"] = n_nuclei_list - adata.obs["nuclei_total_area"] = nuclei_total_area_list - adata.obs["nuclei_mean_area"] = nuclei_mean_area_list - adata.obs["nuclei_std_area"] = nuclei_std_area_list - adata.obs["eccentricity"] = eccentricity_list - adata.obs["mean_pix_r"] = mean_pix_list_r - adata.obs["std_pix_r"] = std_pix_list_r - adata.obs["mean_pix_g"] = mean_pix_list_g - adata.obs["std_pix_g"] = std_pix_list_g - adata.obs["mean_pix_b"] = mean_pix_list_b - adata.obs["std_pix_b"] = std_pix_list_b - adata.obs["nuclei_total_area_per_tile"] = adata.obs["nuclei_total_area"] / 299 / 299 - return adata if copy else None - - -def _calculate_morph_stats(tile_path): - imInput = skimage.io.imread(tile_path) - stain_color_map = htk.preprocessing.color_deconvolution.stain_color_map - stains = [ - "hematoxylin", # nuclei stain - "eosin", # cytoplasm stain - "null", - ] # set to null if input contains only two stains - w_est = htk.preprocessing.color_deconvolution.rgb_separate_stains_macenko_pca( - imInput, 255 - ) - - # Perform color deconvolution - deconv_result = htk.preprocessing.color_deconvolution.color_deconvolution( - imInput, w_est, 255 - ) - - channel = htk.preprocessing.color_deconvolution.find_stain_index( - stain_color_map[stains[0]], w_est - ) - im_nuclei_stain = deconv_result.Stains[:, :, channel] - - thresh = skimage.filters.threshold_otsu(im_nuclei_stain) - # im_fgnd_mask = im_nuclei_stain < thresh - im_fgnd_mask = sp.ndimage.morphology.binary_fill_holes( - im_nuclei_stain < 0.8 * thresh - ) - - distance = ndi.distance_transform_edt(im_fgnd_mask) - coords = peak_local_max(distance, footprint=np.ones((3, 3)), labels=im_fgnd_mask) - mask = np.zeros(distance.shape, dtype=bool) - mask[tuple(coords.T)] = True - markers, _ = ndi.label(mask) - - labels = watershed(im_nuclei_stain, markers, mask=im_fgnd_mask) - min_nucleus_area = 60 - im_nuclei_seg_mask = htk.segmentation.label.area_open( - labels, min_nucleus_area - ).astype(np.int64) - - # compute nuclei properties - objProps = skimage.measure.regionprops(im_nuclei_seg_mask) - - n_nuclei = len(objProps) - - nuclei_total_area = sum(map(lambda x: x.area, objProps)) - nuclei_mean_area = np.mean(list(map(lambda x: x.area, objProps))) - nuclei_std_area = np.std(list(map(lambda x: x.area, objProps))) - - mean_pix = imInput.reshape(3, -1).mean(1) - std_pix = imInput.reshape(3, -1).std(1) - - eccentricity = np.mean(list(map(lambda x: x.eccentricity, objProps))) - - solidity = np.mean(list(map(lambda x: x.solidity, objProps))) - - return ( - n_nuclei, - nuclei_total_area, - nuclei_mean_area, - nuclei_std_area, - eccentricity, - solidity, - mean_pix[0], - std_pix[0], - mean_pix[1], - std_pix[1], - mean_pix[2], - std_pix[2], - ) diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 82723338..880e590b 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -1574,6 +1574,7 @@ def spatialcci_plot_interactive(adata: AnnData): output_notebook() show(bokeh_object.app, notebook_handle=True) + # def het_plot_interactive(adata: AnnData): # bokeh_object = BokehCciPlot(adata) # output_notebook() diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tools/microenv/cci/base.py index 2e852d3a..6a5b5259 100644 --- a/stlearn/tools/microenv/cci/base.py +++ b/stlearn/tools/microenv/cci/base.py @@ -95,7 +95,7 @@ def calc_distance(adata: AnnData, distance: float | None): scalefactors["spot_diameter_fullres"] * scalefactors[ "tissue_" + adata.uns["spatial"][library_id]["use_quality"] + "_scalef" - ] + ] * 2 ) return distance @@ -148,7 +148,7 @@ def get_lrs_scores( new_lrs = np.array( [ - "_".join(spot_lr1s.columns.values[i: i + 2]) + "_".join(spot_lr1s.columns.values[i : i + 2]) for i in range(0, spot_lr1s.shape[1], 2) ] ) @@ -386,7 +386,7 @@ def get_scores( spot_scores = np.zeros((len(spot_indices), spot_lr1s.shape[1] // 2), np.float64) for i in prange(0, spot_lr1s.shape[1] // 2): i_ = i * 2 # equivalent to range(0, spot_lr1s.shape[1], 2) - spot_lr1, spot_lr2 = spot_lr1s[:, i_: (i_ + 2)], spot_lr2s[:, i_: (i_ + 2)] + spot_lr1, spot_lr2 = spot_lr1s[:, i_ : (i_ + 2)], spot_lr2s[:, i_ : (i_ + 2)] lr_scores = lr_core(spot_lr1, spot_lr2, neighbours, min_expr, spot_indices) # The merge scores # lr_scores = np.multiply(het_vals[spot_indices], lr_scores) @@ -446,7 +446,7 @@ def lr_grid( & (coor["imagecol"] < grid[0] + width) & (coor["imagerow"] < grid[1]) & (coor["imagerow"] > grid[1] - height) - ] + ] df_grid.loc[n] = df.loc[spots.index].sum() # expand the LR pairs list by swapping ligand-receptor positions diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index ee44242d..be44ee8d 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -179,9 +179,7 @@ def Read10X( image_path ) else: - raise ValueError( - "Trying to load fulres but no image_path set." - ) + raise ValueError("Trying to load fulres but no image_path set.") image_coor = adata.obsm["spatial"] img = plt.imread(image_path, None) @@ -258,7 +256,7 @@ def ReadOldST( def ReadSlideSeq( count_matrix_file: str | Path, spatial_file: str | Path, - library_id: str| None = None, + library_id: str | None = None, scale: float | None = None, quality: str = "hires", spot_diameter_fullres: float = 50, From d8fff79f93ba1993e2d400c5949babfbe035db60 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 11:05:23 +1000 Subject: [PATCH 036/241] Ignore site packages. --- mypy.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 01f6bb35..d40e59ee 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,3 @@ [mypy] -follow_untyped_imports = True \ No newline at end of file +follow_untyped_imports = True +no_site_packages = True From 0f32bfabadc8af6c1cf6acdcc91439417341f835 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 13:45:30 +1000 Subject: [PATCH 037/241] Fixing types - especially in logging. --- mypy.ini | 1 + stlearn/_settings.py | 50 ++-- stlearn/logging.py | 213 +++++++++++++----- stlearn/plotting/classes.py | 2 +- stlearn/plotting/cluster_plot.py | 2 + stlearn/plotting/gene_plot.py | 59 ++--- stlearn/plotting/trajectory/tree_plot.py | 11 +- .../plotting/trajectory/tree_plot_simple.py | 11 +- 8 files changed, 234 insertions(+), 115 deletions(-) diff --git a/mypy.ini b/mypy.ini index d40e59ee..5d8b6f99 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,3 +1,4 @@ [mypy] follow_untyped_imports = True no_site_packages = True +ignore_missing_imports = True diff --git a/stlearn/_settings.py b/stlearn/_settings.py index b3c06afa..97276dbd 100644 --- a/stlearn/_settings.py +++ b/stlearn/_settings.py @@ -6,7 +6,7 @@ from logging import getLevelName from pathlib import Path from time import time -from typing import Any, TextIO +from typing import Any, TextIO, Iterator from . import logging from ._compat import Literal @@ -15,7 +15,7 @@ # All the code here migrated from scanpy # It help to work with scanpy package -_VERBOSITY_TO_LOGLEVEL = { +_VERBOSITY_TO_LOGLEVEL: dict[str | int, str] = { "error": "ERROR", "warning": "WARNING", "info": "INFO", @@ -40,7 +40,7 @@ def level(self) -> int: return getLevelName(_VERBOSITY_TO_LOGLEVEL[self]) @contextmanager - def override(self, verbosity: "Verbosity") -> AbstractContextManager["Verbosity"]: + def override(self, verbosity: "Verbosity") -> Iterator["Verbosity"]: """\ Temporarily override verbosity """ @@ -66,6 +66,9 @@ class stLearnConfig: # noqa N801 """\ Config manager for scanpy. """ + _logpath: Path | None + _logfile: TextIO + _verbosity: Verbosity def __init__( self, @@ -144,9 +147,9 @@ def verbosity(self, verbosity: Verbosity | int | str): v for v in _VERBOSITY_TO_LOGLEVEL if isinstance(v, str) ] if isinstance(verbosity, Verbosity): - self._verbosity = verbosity + new_verbosity = verbosity elif isinstance(verbosity, int): - self._verbosity = Verbosity(verbosity) + new_verbosity = Verbosity(verbosity) elif isinstance(verbosity, str): verbosity = verbosity.lower() if verbosity not in verbosity_str_options: @@ -155,10 +158,9 @@ def verbosity(self, verbosity: Verbosity | int | str): f"Accepted string values are: {verbosity_str_options}" ) else: - self._verbosity = Verbosity(verbosity_str_options.index(verbosity)) - else: - _type_check(verbosity, "verbosity", (str, int)) - _set_log_level(self, _VERBOSITY_TO_LOGLEVEL[self._verbosity]) + new_verbosity = Verbosity(verbosity_str_options.index(verbosity)) + self._verbosity = new_verbosity + _set_log_level(self, self._verbosity) @property def plot_suffix(self) -> str: @@ -334,10 +336,13 @@ def logpath(self) -> Path | None: @logpath.setter def logpath(self, logpath: str | Path | None): - _type_check(logpath, "logfile", (str, Path)) - # set via “file object” branch of logfile.setter - self.logfile = Path(logpath).open("a") - self._logpath = Path(logpath) + if logpath is None: + self._logpath = None + else: + _type_check(logpath, "logpath", (str, Path)) + # set via “file object” branch of logfile.setter + self.logfile = Path(logpath).open("a") + self._logpath = Path(logpath) @property def logfile(self) -> TextIO: @@ -355,14 +360,17 @@ def logfile(self) -> TextIO: @logfile.setter def logfile(self, logfile: str | Path | TextIO | None): - if not hasattr(logfile, "write") and logfile: - self.logpath = logfile - else: # file object - if not logfile: # None or '' - logfile = sys.stdout if self._is_run_from_ipython() else sys.stderr + if logfile is None or logfile == "": + self._logfile = sys.stdout if self._is_run_from_ipython() else sys.stderr + self._logpath = None + elif isinstance(logfile, (str, Path)): + path = Path(logfile) + self._logfile = path.open("a") + self._logpath = path + elif isinstance(logfile, TextIO): self._logfile = logfile self._logpath = None - _set_log_file(self) + _set_log_file(self) @property def categories_to_ignore(self) -> list[str]: @@ -443,9 +451,7 @@ def set_figure_params( try: import IPython - if isinstance(ipython_format, str): - ipython_format = [ipython_format] - IPython.display.set_matplotlib_formats(*ipython_format) + IPython.display.set_matplotlib_formats(*[ipython_format]) except Exception: pass from matplotlib import rcParams diff --git a/stlearn/logging.py b/stlearn/logging.py index 24421cc2..cfacdfad 100644 --- a/stlearn/logging.py +++ b/stlearn/logging.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta, timezone from functools import partial, update_wrapper from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING +from typing import Dict, Any, Optional, overload, Mapping, Union import anndata.logging @@ -11,25 +12,34 @@ logging.addLevelName(HINT, "HINT") +class CustomLogRecord(logging.LogRecord): + """Custom root logger that maintains compatibility with standard logging interface.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.time_passed: Optional[timedelta] = None + self.deep: Optional[str] = None + + class _RootLogger(logging.RootLogger): def __init__(self, level): super().__init__(level) self.propagate = False _RootLogger.manager = logging.Manager(self) - def log( - self, - level: int, - msg: str, - *, - extra: dict | None = None, - time: datetime = None, - deep: str | None = None, + def log_with_timing( + self, + level: int, + msg: str, + *, + extra: dict | None = None, + time: datetime | None = None, + deep: str | None = None, ) -> datetime: from . import settings now = datetime.now(timezone.utc) - time_passed: timedelta = None if time is None else now - time + time_passed: Optional[timedelta] = None if time is None else now - time extra = { **(extra or {}), "deep": deep if settings.verbosity.level < level else None, @@ -38,23 +48,101 @@ def log( super().log(level, msg, extra=extra) return now - def critical(self, msg, *, time=None, deep=None, extra=None) -> datetime: - return self.log(CRITICAL, msg, time=time, deep=deep, extra=extra) - - def error(self, msg, *, time=None, deep=None, extra=None) -> datetime: - return self.log(ERROR, msg, time=time, deep=deep, extra=extra) - - def warning(self, msg, *, time=None, deep=None, extra=None) -> datetime: - return self.log(WARNING, msg, time=time, deep=deep, extra=extra) - - def info(self, msg, *, time=None, deep=None, extra=None) -> datetime: - return self.log(INFO, msg, time=time, deep=deep, extra=extra) - - def hint(self, msg, *, time=None, deep=None, extra=None) -> datetime: - return self.log(HINT, msg, time=time, deep=deep, extra=extra) - - def debug(self, msg, *, time=None, deep=None, extra=None) -> datetime: - return self.log(DEBUG, msg, time=time, deep=deep, extra=extra) + def _handle_enhanced_logging(self, level: int, msg, *args, **kwargs) -> Optional[ + datetime]: + """Handle logging with enhanced features (timing, deep info) or fall back to standard logging.""" + if 'time' in kwargs or 'deep' in kwargs or 'extra' in kwargs: + # Extract enhanced arguments + time_arg = kwargs.pop('time', None) + deep_arg = kwargs.pop('deep', None) + extra_arg = kwargs.pop('extra', None) + + # Format message if there are remaining args + if args or kwargs: + formatted_msg = msg % args if args else msg + else: + formatted_msg = msg + + return self.log_with_timing(level, formatted_msg, + time=time_arg, deep=deep_arg, extra=extra_arg) + else: + super().log(level, msg, *args, **kwargs) + return None + + def hint(self, msg, *, time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None) -> datetime: + return self.log_with_timing(HINT, msg, time=time, deep=deep, extra=extra) + + @overload + def debug(self, msg: object, *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None) -> None: + ... + + @overload + def debug(self, msg, *args, **kwargs): + ... + + def debug(self, msg, *args, **kwargs) -> Optional[datetime]: + return self._handle_enhanced_logging(DEBUG, msg, *args, **kwargs) + + @overload + def info(self, msg: object, *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None) -> None: + ... + + @overload + def info(self, msg, *args, **kwargs): + ... + + def info(self, msg, *args, **kwargs) -> Optional[datetime]: + return self._handle_enhanced_logging(INFO, msg, *args, **kwargs) + + @overload + def warning(self, msg: object, *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None) -> None: + ... + + @overload + def warning(self, msg, *args, **kwargs): + ... + + def warning(self, msg, *args, **kwargs) -> Optional[datetime]: + return self._handle_enhanced_logging(WARNING, msg, *args, **kwargs) + + @overload + def error(self, msg: object, *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None) -> None: + ... + + @overload + def error(self, msg, *args, **kwargs): + ... + + def error(self, msg, *args, **kwargs) -> Optional[datetime]: + return self._handle_enhanced_logging(ERROR, msg, *args, **kwargs) + + @overload + def critical(self, msg: object, *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None) -> None: + ... + + @overload + def critical(self, msg, *args, **kwargs): + ... + + def critical(self, msg, *args, **kwargs) -> Optional[datetime]: + return self._handle_enhanced_logging(CRITICAL, msg, *args, **kwargs) def _set_log_file(settings): @@ -80,7 +168,7 @@ def _set_log_level(settings, level: int): class _LogFormatter(logging.Formatter): def __init__( - self, fmt="{levelname}: {message}", datefmt="%Y-%m-%d %H:%M", style="{" + self, fmt="{levelname}: {message}", datefmt="%Y-%m-%d %H:%M", style="{" ): super().__init__(fmt, datefmt, style) @@ -92,20 +180,28 @@ def format(self, record: logging.LogRecord): self._style._fmt = "--> {message}" elif record.levelno == DEBUG: self._style._fmt = " {message}" - if record.time_passed: - # strip microseconds - if record.time_passed.microseconds: - record.time_passed = timedelta( - seconds=int(record.time_passed.total_seconds()) + + # Handle time_passed if present (should be in extra) + time_passed = getattr(record, 'time_passed', None) + if time_passed: + # Strip microseconds + if time_passed.microseconds: + time_passed = timedelta( + seconds=int(time_passed.total_seconds()) ) if "{time_passed}" in record.msg: record.msg = record.msg.replace( - "{time_passed}", str(record.time_passed) + "{time_passed}", str(time_passed) ) else: self._style._fmt += " ({time_passed})" - if record.deep: - record.msg = f"{record.msg}: {record.deep}" + # Add time_passed to record for formatting + record.time_passed = time_passed + + deep = getattr(record, 'deep', None) + if deep: + record.msg = f"{record.msg}: {deep}" + result = logging.Formatter.format(self, record) self._style._fmt = format_orig return result @@ -114,7 +210,6 @@ def format(self, record: logging.LogRecord): print_memory_usage = anndata.logging.print_memory_usage get_memory_usage = anndata.logging.get_memory_usage - _DEPENDENCIES_NUMERICS = [ "anndata", # anndata actually shouldn't, but as long as it's in development "umap", @@ -127,7 +222,6 @@ def format(self, record: logging.LogRecord): "louvain", ] - _DEPENDENCIES_PLOTTING = ["matplotlib", "seaborn"] @@ -171,15 +265,16 @@ def print_version_and_date(): def _copy_docs_and_signature(fn): + """Copy documentation and signature from function.""" return partial(update_wrapper, wrapped=fn, assigned=["__doc__", "__annotations__"]) def error( - msg: str, - *, - time: datetime | None = None, - deep: str | None = None, - extra: dict | None = None, + msg: str, + *, + time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, ) -> datetime: """\ Log message with specific level and return current time. @@ -194,39 +289,47 @@ def error( If `msg` contains `{time_passed}`, the time difference is instead inserted at that position. deep - If the current verbosity is higher than the log function’s level, + If the current verbosity is higher than the log function's level, this gets displayed as well extra Additional values you can specify in `msg` like `{time_passed}`. """ from ._settings import settings - return settings._root_logger.error(msg, time=time, deep=deep, extra=extra) + result = settings._root_logger.error(msg, time=time, deep=deep, extra=extra) + return result or datetime.now(timezone.utc) @_copy_docs_and_signature(error) -def warning(msg, *, time=None, deep=None, extra=None) -> datetime: +def warning(msg: str, *, time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None) -> datetime: from ._settings import settings - - return settings._root_logger.warning(msg, time=time, deep=deep, extra=extra) + result = settings._root_logger.warning(msg, time=time, deep=deep, extra=extra) + return result or datetime.now(timezone.utc) @_copy_docs_and_signature(error) -def info(msg, *, time=None, deep=None, extra=None) -> datetime: +def info(msg: str, *, time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None) -> datetime: from ._settings import settings - - return settings._root_logger.info(msg, time=time, deep=deep, extra=extra) + result = settings._root_logger.info(msg, time=time, deep=deep, extra=extra) + return result or datetime.now(timezone.utc) @_copy_docs_and_signature(error) -def hint(msg, *, time=None, deep=None, extra=None) -> datetime: +def hint(msg: str, *, time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None) -> datetime: from ._settings import settings - return settings._root_logger.hint(msg, time=time, deep=deep, extra=extra) @_copy_docs_and_signature(error) -def debug(msg, *, time=None, deep=None, extra=None) -> datetime: +def debug(msg: str, *, time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None) -> datetime: from ._settings import settings - - return settings._root_logger.debug(msg, time=time, deep=deep, extra=extra) + result = settings._root_logger.debug(msg, time=time, deep=deep, extra=extra) + return result or datetime.now(timezone.utc) diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 4eb24266..108012a9 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -251,7 +251,7 @@ def __init__( fname: str | None = None, dpi: int | None = 120, # gene plot param - gene_symbols: str | list = None, + gene_symbols: str | list | None = None, threshold: float | None = None, method: str = "CumSum", contour: bool = False, diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index e2c2e0c1..5ed28cb8 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -112,6 +112,8 @@ def cluster_plot( trajectory_arrowsize=trajectory_arrowsize, ) + return adata + def cluster_plot_interactive( adata: AnnData, diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index 290d33e9..419c4200 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -15,35 +15,35 @@ @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) def gene_plot( - adata: AnnData, - gene_symbols: str | list = None, - threshold: float | None = None, - method: str = "CumSum", - contour: bool = False, - step_size: int | None = None, - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - zoom_coord: float | None = None, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 0.7, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - vmin: float | None = None, - vmax: float | None = None, + adata: AnnData, + gene_symbols: str | list | None = None, + threshold: float | None = None, + method: str = "CumSum", + contour: bool = False, + step_size: int | None = None, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str | None = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool | None = True, + show_axis: bool | None = False, + show_image: bool | None = True, + show_color_bar: bool | None = True, + color_bar_label: str | None = "", + zoom_coord: float | None = None, + crop: bool | None = True, + margin: float | None = 100, + size: float | None = 7, + image_alpha: float | None = 1.0, + cell_alpha: float | None = 0.7, + use_raw: bool | None = False, + fname: str | None = None, + dpi: int | None = 120, + vmin: float | None = None, + vmax: float | None = None, ) -> AnnData | None: """\ Allows the visualization of a single gene or multiple genes as the values @@ -94,6 +94,7 @@ def gene_plot( vmin=vmin, vmax=vmax, ) + return adata def gene_plot_interactive(adata: AnnData): diff --git a/stlearn/plotting/trajectory/tree_plot.py b/stlearn/plotting/trajectory/tree_plot.py index 71992608..88583991 100644 --- a/stlearn/plotting/trajectory/tree_plot.py +++ b/stlearn/plotting/trajectory/tree_plot.py @@ -1,5 +1,6 @@ import math import random +from typing import Tuple import networkx as nx from anndata import AnnData @@ -10,16 +11,16 @@ def tree_plot( adata: AnnData, - library_id: str = None, - figsize: float | int = (10, 4), + library_id: str | None = None, + figsize: Tuple[float, float] = (10, 4), data_alpha: float = 1.0, use_label: str = "louvain", spot_size: float | int = 50, fontsize: int = 6, piesize: float = 0.15, zoom: float = 0.1, - name: str = None, - output: str = None, + name: str | None = None, + output: str | None = None, dpi: int = 180, show_all: bool = False, show_plot: bool = True, @@ -104,6 +105,8 @@ def tree_plot( if show_plot: plt.show() + return adata + def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5): """ diff --git a/stlearn/plotting/trajectory/tree_plot_simple.py b/stlearn/plotting/trajectory/tree_plot_simple.py index 92e0cb07..03f1b7bd 100644 --- a/stlearn/plotting/trajectory/tree_plot_simple.py +++ b/stlearn/plotting/trajectory/tree_plot_simple.py @@ -1,5 +1,6 @@ import math import random +from typing import Tuple import networkx as nx from anndata import AnnData @@ -10,16 +11,16 @@ def tree_plot_simple( adata: AnnData, - library_id: str = None, - figsize: float | int = (10, 4), + library_id: str | None = None, + figsize: Tuple[float, float] = (10, 4), data_alpha: float = 1.0, use_label: str = "louvain", spot_size: float | int = 50, fontsize: int = 6, piesize: float = 0.15, zoom: float = 0.1, - name: str = None, - output: str = None, + name: str | None = None, + output: str | None = None, dpi: int = 180, show_all: bool = False, show_plot: bool = True, @@ -104,6 +105,8 @@ def tree_plot_simple( if show_plot: plt.show() + return adata + def hierarchy_pos(G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, xcenter=0.5): """ From eb94410424d07ef31212b67b9172dcde9b15c607 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 15:12:34 +1000 Subject: [PATCH 038/241] Fixing types. --- stlearn/classes.py | 2 + stlearn/plotting/cci_plot.py | 62 +-- stlearn/plotting/classes.py | 500 +++++++++--------- stlearn/plotting/classes_bokeh.py | 26 +- stlearn/plotting/cluster_plot.py | 52 +- stlearn/plotting/feat_plot.py | 34 +- stlearn/plotting/gene_plot.py | 34 +- stlearn/plotting/subcluster_plot.py | 30 +- .../spatials/trajectory/pseudotimespace.py | 6 +- stlearn/tools/microenv/cci/perm_utils.py | 2 +- stlearn/tools/microenv/cci/permutation.py | 25 +- stlearn/utils.py | 13 +- 12 files changed, 412 insertions(+), 374 deletions(-) diff --git a/stlearn/classes.py b/stlearn/classes.py index 12c25ede..2c9026e9 100644 --- a/stlearn/classes.py +++ b/stlearn/classes.py @@ -19,6 +19,8 @@ class Spatial: + img: np.ndarray | None + def __init__( self, adata: AnnData, diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 880e590b..5c053747 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -3,7 +3,7 @@ import sys from typing import ( Any, - Optional, # Special + Optional, Tuple, # Special ) import matplotlib @@ -424,24 +424,24 @@ def lr_result_plot( use_lr: Optional["str"] = None, use_result: Optional["str"] = "lr_sig_scores", # plotting param - title: Optional["str"] = None, + title: str | None = None, figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", + cmap: str = "Spectral_r", ax: plt_axis.Axes | None = None, fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - zoom_coord: float | None = None, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, fname: str | None = None, - dpi: int | None = 120, + dpi: int = 120, contour: bool = False, step_size: int | None = None, vmin: float | None = None, @@ -474,6 +474,8 @@ def lr_result_plot( Whether to show axis or not. show_image: bool Whether to plot the image. + zoom_coord: Tuple[float, float, float, float] + Bounding box of plot. show_color_bar: bool Whether to show the color bar. crop: bool @@ -890,28 +892,28 @@ def lr_plot( def het_plot( adata: AnnData, # plotting param - title: Optional["str"] = None, + title: str | None = None, figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", + cmap: str = "Spectral_r", use_label: str | None = None, list_clusters: list | None = None, ax: plt_axis.Axes | None = None, fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - zoom_coord: float | None = None, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, fname: str | None = None, - dpi: int | None = 120, + dpi: int = 120, # cci_rank param - use_het: str | None = "het", + use_het: str = "het", contour: bool = False, step_size: int | None = None, vmin: float | None = None, diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 108012a9..6a6db687 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -7,7 +7,7 @@ import numbers import warnings from typing import ( # Special - Optional, # Classes + Optional, Tuple, # Classes ) import matplotlib @@ -18,38 +18,38 @@ from anndata import AnnData from scipy.interpolate import griddata +from .utils import centroidpython, check_sublist, get_cluster, get_cmap, get_node from ..classes import Spatial from ..utils import Axes, _AxesSubplot, _read_graph -from .utils import centroidpython, check_sublist, get_cluster, get_cmap, get_node class SpatialBasePlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - zoom_coord: float | None = None, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 0.7, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - **kwds, + self, + # plotting param + adata: AnnData, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + zoom_coord: Tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 0.7, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + **kwds, ): super().__init__( adata, @@ -73,7 +73,7 @@ def __init__( if use_label is not None: assert ( - use_label in self.adata[0].obs.columns + use_label in self.adata[0].obs.columns ), "Please choose the right label in `adata.obs.columns`!" self.use_label = use_label @@ -103,8 +103,8 @@ def __init__( stlearn_cmap = ["jana_40", "default"] cmap_available = plt.colormaps() + scanpy_cmap + stlearn_cmap error_msg = ( - "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" - "one of these: " + str(cmap_available) + "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" + "one of these: " + str(cmap_available) ) if cmap is str: assert cmap in cmap_available, error_msg @@ -139,7 +139,7 @@ def create_query(list_cl, use_label): if self.list_clusters is not None: # IF not all clusters specified, subset, otherwise just copy. if len(self.list_clusters) != len( - self.adata[0].obs[self.use_label].cat.categories + self.adata[0].obs[self.use_label].cat.categories ): self.query_adata = self.query_adata[ self.query_adata.obs.query( @@ -178,12 +178,11 @@ def _remove_axis(self, main_ax: Axes): def _crop_image(self, main_ax: _AxesSubplot, margin: float): main_ax.set_xlim(self.imagecol.min() - margin, self.imagecol.max() + margin) - main_ax.set_ylim(self.imagerow.min() - margin, self.imagerow.max() + margin) - main_ax.set_ylim(main_ax.get_ylim()[::-1]) - def _zoom_image(self, main_ax: _AxesSubplot, zoom_coord: float | None): + def _zoom_image(self, main_ax: _AxesSubplot, + zoom_coord: Tuple[float, float, float, float]): main_ax.set_xlim(zoom_coord[0], zoom_coord[1]) main_ax.set_ylim(zoom_coord[3], zoom_coord[2]) @@ -225,40 +224,42 @@ def _save_output(self): class GenePlot(SpatialBasePlot): + gene_symbols: list[str] + def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - crop: bool | None = True, - zoom_coord: float | None = None, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # gene plot param - gene_symbols: str | list | None = None, - threshold: float | None = None, - method: str = "CumSum", - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: str | None = None, + figsize: Tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # gene plot param + gene_symbols: str | list[str] | None = None, + threshold: float | None = None, + method: str = "CumSum", + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, + **kwargs, ): super().__init__( adata=adata, @@ -292,17 +293,18 @@ def __init__( self.step_size = step_size + if isinstance(gene_symbols, str): + self.gene_symbols = [gene_symbols] + elif gene_symbols is None: + self.gene_symbols = [] + else: + self.gene_symbols = gene_symbols + if self.title is None: - if gene_symbols is str: - self.title = str(gene_symbols) - gene_symbols = [gene_symbols] - else: - self.title = ", ".join(gene_symbols) + self.title = ", ".join(self.gene_symbols) self._add_title() - self.gene_symbols = gene_symbols - gene_values = self._get_gene_expression() self.available_ids = self._add_threshold(gene_values, threshold) @@ -438,38 +440,38 @@ def _add_threshold(self, gene_values, threshold): class FeaturePlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - crop: bool | None = True, - zoom_coord: float | None = None, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # gene plot param - feature: str = None, - threshold: float | None = None, - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # gene plot param + feature: str | None = None, + threshold: float | None = None, + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, + **kwargs, ): super().__init__( adata=adata, @@ -526,7 +528,7 @@ def _get_feature_values(self): self.feature + " is not in data.obs, please try another feature" ) elif not isinstance( - self.query_adata.obs[self.feature].values[0], numbers.Number + self.query_adata.obs[self.feature].values[0], numbers.Number ): raise ValueError( self.feature @@ -602,44 +604,44 @@ def _add_threshold(self, feature_values, threshold): # Cluster plot class class ClusterPlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "default", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - zoom_coord: float | None = None, - margin: float | None = 100, - size: float | None = 5, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - fname: str | None = None, - dpi: int | None = 120, - # cluster plot param - show_subcluster: bool | None = False, - show_cluster_labels: bool | None = False, - show_trajectories: bool | None = False, - reverse: bool | None = False, - show_node: bool | None = False, - threshold_spots: int | None = 5, - text_box_size: float | None = 5, - color_bar_size: float | None = 10, - bbox_to_anchor: tuple[float, float] | None = (1, 1), - # trajectory - trajectory_node_size: int | None = 10, - trajectory_alpha: float | None = 1.0, - trajectory_width: float | None = 2.5, - trajectory_edge_color: str | None = "#f4efd3", - trajectory_arrowsize: int | None = 17, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "default", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 5, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + fname: str | None = None, + dpi: int = 120, + # cluster plot param + show_subcluster: bool = False, + show_cluster_labels: bool = False, + show_trajectories: bool = False, + reverse: bool = False, + show_node: bool = False, + threshold_spots: int = 5, + text_box_size: float = 5, + color_bar_size: float = 10, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + # trajectory + trajectory_node_size: int = 10, + trajectory_alpha: float = 1.0, + trajectory_width: float = 2.5, + trajectory_edge_color: str = "#f4efd3", + trajectory_arrowsize: int = 17, ): super().__init__( adata=adata, @@ -764,7 +766,7 @@ def _add_cluster_labels(self): label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ].index + ].index ) subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), label_index) @@ -807,7 +809,7 @@ def _add_sub_clusters(self): label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ].index + ].index ) subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), label_index) @@ -817,18 +819,18 @@ def _add_sub_clusters(self): imgrow_new = subset_spatial[:, 1] * self.scale_factor if ( - len( - self.query_adata.obs[ - self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"].unique() - ) - < 2 + len( + self.query_adata.obs[ + self.query_adata.obs[self.use_label] == str(label) + ]["sub_cluster_labels"].unique() + ) + < 2 ): centroids = [centroidpython(imgcol_new, imgrow_new)] classes = np.array( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"].unique() + ]["sub_cluster_labels"].unique() ) else: @@ -839,7 +841,7 @@ def _add_sub_clusters(self): np.column_stack((imgcol_new, imgrow_new)), self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"], + ]["sub_cluster_labels"], ) centroids = clf.centroids_ @@ -847,12 +849,12 @@ def _add_sub_clusters(self): for j, label in enumerate(classes): if ( - len( - self.query_adata.obs[ - self.query_adata.obs["sub_cluster_labels"] == label - ] - ) - > self.threshold_spots + len( + self.query_adata.obs[ + self.query_adata.obs["sub_cluster_labels"] == label + ] + ) + > self.threshold_spots ): if centroids[j][0] < 1500: x = -100 @@ -954,34 +956,34 @@ def _add_trajectories(self): class SubClusterPlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "jet", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - zoom_coord: float | None = None, - margin: float | None = 100, - size: float | None = 5, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - fname: str | None = None, - dpi: int | None = 120, - # subcluster plot param - cluster: int | None = 0, - threshold_spots: int | None = 5, - text_box_size: float | None = 5, - bbox_to_anchor: tuple[float, float] | None = (1, 1), - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "jet", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 5, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + fname: str | None = None, + dpi: int = 120, + # subcluster plot param + cluster: int = 0, + threshold_spots: int = 5, + text_box_size: float = 5, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + **kwargs, ): super().__init__( adata=adata, @@ -1108,36 +1110,36 @@ def _add_subclusters_label(self, subset): class CciPlot(GenePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - zoom_coord: float | None = None, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # cci_rank param - use_het: str | None = "het", - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # cci_rank param + use_het: str = "het", + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, + **kwargs, ): super().__init__( adata=adata, @@ -1177,36 +1179,36 @@ def _get_gene_expression(self): class LrResultPlot(GenePlot): def __init__( - self, - adata: AnnData, - use_lr: Optional["str"] = None, - use_result: Optional["str"] = "lr_sig_scores", - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - zoom_coord: float | None = None, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, - use_raw: bool | None = False, - fname: str | None = None, - dpi: int | None = 120, - # cci_rank param - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, - **kwargs, + self, + adata: AnnData, + use_lr: Optional["str"] = None, + use_result: Optional["str"] = "lr_sig_scores", + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # cci_rank param + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, + **kwargs, ): # Making sure cci_rank has been run first # if "lr_summary" not in adata.uns: diff --git a/stlearn/plotting/classes_bokeh.py b/stlearn/plotting/classes_bokeh.py index 8b5d6fd0..2aff9167 100644 --- a/stlearn/plotting/classes_bokeh.py +++ b/stlearn/plotting/classes_bokeh.py @@ -57,7 +57,10 @@ def __init__( adata, ) # Open image, and make sure it's RGB*A* - image = (self.img * 255).astype(np.uint8) + if self.img is None: + raise ValueError("self.img must be a numpy array") + else: + image = (self.img * 255).astype(np.uint8) img_pillow = Image.fromarray(image).convert("RGBA") @@ -328,7 +331,10 @@ def __init__( super().__init__(adata) # Open image, and make sure it's RGB*A* - image = (self.img * 255).astype(np.uint8) + if self.img is None: + raise ValueError("self.img must be a numpy array") + else: + image = (self.img * 255).astype(np.uint8) img_pillow = Image.fromarray(image).convert("RGBA") @@ -771,7 +777,11 @@ def __init__( adata, ) # Open image, and make sure it's RGB*A* - image = (self.img * 255).astype(np.uint8) + if self.img is None: + raise ValueError("self.img must be a numpy array") + else: + image = (self.img * 255).astype(np.uint8) + img_pillow = Image.fromarray(image).convert("RGBA") @@ -949,7 +959,10 @@ def __init__( adata, ) # Open image, and make sure it's RGB*A* - image = (self.img * 255).astype(np.uint8) + if self.img is None: + raise ValueError("self.img must be a numpy array") + else: + image = (self.img * 255).astype(np.uint8) img_pillow = Image.fromarray(image).convert("RGBA") @@ -1229,7 +1242,10 @@ def __init__( ): super().__init__(adata) # Open image, and make sure it's RGB*A* - image = (self.img * 255).astype(np.uint8) + if self.img is None: + raise ValueError("self.img must be a numpy array") + else: + image = (self.img * 255).astype(np.uint8) img_pillow = Image.fromarray(image).convert("RGBA") diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index 5ed28cb8..8c3194e0 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -1,5 +1,5 @@ from typing import ( - Optional, # Special + Optional, Tuple, # Special ) import matplotlib @@ -19,39 +19,39 @@ def cluster_plot( # plotting param title: Optional["str"] = None, figsize: tuple[float, float] | None = None, - cmap: str | None = "default", + cmap: str = "default", use_label: str | None = None, list_clusters: list | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - zoom_coord: float | None = None, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 5, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 5, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, fname: str | None = None, - dpi: int | None = 120, + dpi: int = 120, # cluster plot param - show_subcluster: bool | None = False, - show_cluster_labels: bool | None = False, - show_trajectories: bool | None = False, - reverse: bool | None = False, - show_node: bool | None = False, - threshold_spots: int | None = 5, - text_box_size: float | None = 5, - color_bar_size: float | None = 10, + show_subcluster: bool = False, + show_cluster_labels: bool = False, + show_trajectories: bool = False, + reverse: bool = False, + show_node: bool = False, + threshold_spots: int = 5, + text_box_size: float = 5, + color_bar_size: float= 10, bbox_to_anchor: tuple[float, float] | None = (1, 1), # trajectory - trajectory_node_size: int | None = 10, - trajectory_alpha: float | None = 1.0, - trajectory_width: float | None = 2.5, - trajectory_edge_color: str | None = "#f4efd3", - trajectory_arrowsize: int | None = 17, + trajectory_node_size: int = 10, + trajectory_alpha: float = 1.0, + trajectory_width: float = 2.5, + trajectory_edge_color: str = "#f4efd3", + trajectory_arrowsize: int = 17, ) -> AnnData | None: """\ Allows the visualization of a cluster results as the discretes values diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index d469ab8c..e4256bc9 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -3,7 +3,7 @@ """ from typing import ( - Optional, # Special + Optional, Tuple, # Special ) import matplotlib @@ -15,31 +15,31 @@ # @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) def feat_plot( adata: AnnData, - feature: str = None, + feature: str | None = None, threshold: float | None = None, contour: bool = False, step_size: int | None = None, title: Optional["str"] = None, figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", + cmap: str = "Spectral_r", use_label: str | None = None, list_clusters: list | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - zoom_coord: float | None = None, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 0.7, - use_raw: bool | None = False, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + zoom_coord: Tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 0.7, + use_raw: bool = False, fname: str | None = None, - dpi: int | None = 120, + dpi: int = 120, vmin: float | None = None, vmax: float | None = None, ) -> AnnData | None: @@ -90,3 +90,5 @@ def feat_plot( vmin=vmin, vmax=vmax, ) + + return adata diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index 419c4200..7b5c3285 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -1,5 +1,5 @@ from typing import ( # Special - Optional, # Classes + Optional, Tuple, # Classes ) import matplotlib @@ -21,27 +21,27 @@ def gene_plot( method: str = "CumSum", contour: bool = False, step_size: int | None = None, - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str | None = "Spectral_r", + title: str | None = None, + figsize: Tuple[float, float] | None = None, + cmap: str = "Spectral_r", use_label: str | None = None, list_clusters: list | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - color_bar_label: str | None = "", - zoom_coord: float | None = None, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 7, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 0.7, - use_raw: bool | None = False, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + zoom_coord: Tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 0.7, + use_raw: bool = False, fname: str | None = None, - dpi: int | None = 120, + dpi: int = 120, vmin: float | None = None, vmax: float | None = None, ) -> AnnData | None: diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/plotting/subcluster_plot.py index 4dcef013..c6f9c77c 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/plotting/subcluster_plot.py @@ -17,25 +17,25 @@ def subcluster_plot( # plotting param title: Optional["str"] = None, figsize: tuple[float, float] | None = None, - cmap: str | None = "jet", + cmap: str = "jet", use_label: str | None = None, list_clusters: list | None = None, ax: _AxesSubplot | None = None, - show_plot: bool | None = True, - show_axis: bool | None = False, - show_image: bool | None = True, - show_color_bar: bool | None = True, - crop: bool | None = True, - margin: float | None = 100, - size: float | None = 5, - image_alpha: float | None = 1.0, - cell_alpha: float | None = 1.0, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + margin: float = 100, + size: float = 5, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, fname: str | None = None, - dpi: int | None = 120, + dpi: int = 120, # subcluster plot param - cluster: int | None = 0, - threshold_spots: int | None = 5, - text_box_size: float | None = 5, + cluster: int = 0, + threshold_spots: int = 5, + text_box_size: float= 5, bbox_to_anchor: tuple[float, float] | None = (1, 1), ) -> AnnData | None: """\ @@ -86,3 +86,5 @@ def subcluster_plot( cluster=cluster, threshold_spots=threshold_spots, ) + + return adata \ No newline at end of file diff --git a/stlearn/spatials/trajectory/pseudotimespace.py b/stlearn/spatials/trajectory/pseudotimespace.py index 2214c401..1dec4db9 100644 --- a/stlearn/spatials/trajectory/pseudotimespace.py +++ b/stlearn/spatials/trajectory/pseudotimespace.py @@ -68,12 +68,14 @@ def pseudotimespace_global( n_dims=n_dims, ) + return adata + def pseudotimespace_local( adata: AnnData, use_label: str = "louvain", cluster=None, - w: float = None, + w: float | None = None, ) -> AnnData | None: """\ Perform pseudo-time-space analysis with local level. @@ -99,3 +101,5 @@ def pseudotimespace_local( w = weight_optimizing_local(adata, use_label=use_label, cluster=cluster) local_level(adata, use_label=use_label, cluster=cluster, w=w) + + return adata \ No newline at end of file diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index 4e147b07..63b048b8 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -226,7 +226,7 @@ def get_similar_genesFAST( ref_quants: np.array, n_genes: int, candidate_quants: np.ndarray, - candidate_genes: np.array, + candidate_genes: np.ndarray, ): """Fast version of the above with parallelisation.""" diff --git a/stlearn/tools/microenv/cci/permutation.py b/stlearn/tools/microenv/cci/permutation.py index 267b224c..46491b0b 100644 --- a/stlearn/tools/microenv/cci/permutation.py +++ b/stlearn/tools/microenv/cci/permutation.py @@ -1,6 +1,7 @@ import os import random import sys +from typing import Any import numpy as np import pandas as pd @@ -95,9 +96,9 @@ def perform_spot_testing( bar_format="{l_bar}{bar} [ time left: {remaining} ]", disable=verbose is False, ) as pbar: - - gene_bg_genes = {} # Keep track of genes which can be used to gen. rand-pairs. - spot_lr_indices = [ + # Keep track of genes which can be used to gen. rand-pairs. + gene_bg_genes: dict[str, np.ndarray] = {} + spot_lr_indices: List[List[Any]] = [ [] for i in range(lr_scores.shape[0]) ] # tracks the lrs tested in a given spot for MHT !!!! for lr_j in range(lr_scores.shape[1]): @@ -215,11 +216,11 @@ def perform_perm_testing( adata: AnnData, lr_scores: np.ndarray, n_pairs: int, - lrs: np.array, + lrs: np.ndarray, lr_mid_dist: int, verbose: float, neighbours: List, - het_vals: np.array, + het_vals: np.ndarray, min_expr: float, neg_binom: bool, adj_method: str, @@ -343,14 +344,14 @@ def perform_perm_testing( def permutation( adata: AnnData, n_pairs: int = 200, - distance: int = None, + distance: int = 30, use_lr: str = "cci_lr", - use_het: str = None, + use_het: str | None = None, neg_binom: bool = False, adj_method: str = "fdr", - neighbours: list = None, + neighbours: list | None = None, run_fast: bool = True, - bg_pairs: list = None, + bg_pairs: list | None = None, background: np.array = None, **kwargs, ) -> AnnData: @@ -448,7 +449,7 @@ def permutation( # Negative Binomial fit pvals, pvals_adj, log10_pvals, lr_sign = get_stats( - scores, background, neg_binom, adj_method + scores, background, neg_binom, adj_method=adj_method ) if use_het is not None: @@ -577,8 +578,8 @@ def get_rand_pairs( adata: AnnData, genes: np.array, n_pairs: int, - lrs: list = None, - im: int = None, + lrs: list, + im: int | None = None, ): """Gets equivalent random gene pairs for the inputted lr pair. Parameters diff --git a/stlearn/utils.py b/stlearn/utils.py index 0a76aec7..581aec49 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -96,13 +96,20 @@ def _check_img( def _check_coords( - obsm: Mapping | None, scale_factor: float | None -) -> tuple[np.ndarray | None, np.ndarray | None]: + obsm: Mapping | None, scale_factor: float | None +) -> tuple[np.ndarray, np.ndarray]: + if obsm is None: + raise ValueError("obsm cannot be None") + if scale_factor is None: + raise ValueError("scale_factor cannot be None") + if "spatial" not in obsm: + raise ValueError("'spatial' key not found in obsm") + image_coor = obsm["spatial"] * scale_factor imagecol = image_coor[:, 0] imagerow = image_coor[:, 1] - return [imagecol, imagerow] + return (imagecol, imagerow) def _read_graph(adata: AnnData, graph_type: str | None): From 5b835ded6a27ebf6032a2d37631e07ec35e8a0c9 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 15:56:21 +1000 Subject: [PATCH 039/241] Fixing types. --- stlearn/adds/add_mask.py | 16 +++++------ stlearn/adds/add_positions.py | 4 +-- stlearn/classes.py | 10 +++---- stlearn/embedding/umap.py | 2 ++ stlearn/plotting/cci_plot_helpers.py | 27 ++++++++++--------- stlearn/plotting/mask_plot.py | 20 +++++++------- .../plotting/trajectory/DE_transition_plot.py | 6 +++-- stlearn/plotting/trajectory/local_plot.py | 8 +++--- .../plotting/trajectory/pseudotime_plot.py | 16 ++++++----- .../trajectory/transition_markers_plot.py | 18 ++++++------- stlearn/preprocessing/graph.py | 2 ++ stlearn/preprocessing/normalize.py | 6 +++-- stlearn/spatials/SME/_weighting_matrix.py | 4 ++- stlearn/spatials/clustering/localization.py | 2 +- stlearn/spatials/trajectory/global_level.py | 15 ++++++----- stlearn/spatials/trajectory/pseudotime.py | 2 +- stlearn/tools/clustering/louvain.py | 16 +++-------- stlearn/tools/microenv/cci/analysis.py | 2 +- stlearn/tools/microenv/cci/base.py | 10 +++---- 19 files changed, 101 insertions(+), 85 deletions(-) diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index ffc58f51..dd086217 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -77,9 +77,9 @@ def add_mask( def apply_mask( adata: AnnData, - masks: list | None = "all", + masks: list | str = "all", select: str = "black", - cmap: str = "default", + cmap_name: str = "default", copy: bool = False, ) -> AnnData | None: """\ @@ -108,17 +108,17 @@ def apply_mask( from stlearn.plotting import palettes_st - if cmap == "vega_10_scanpy": + if cmap_name == "vega_10_scanpy": cmap = palettes.vega_10_scanpy - elif cmap == "vega_20_scanpy": + elif cmap_name == "vega_20_scanpy": cmap = palettes.vega_20_scanpy - elif cmap == "default_102": + elif cmap_name == "default_102": cmap = palettes.default_102 - elif cmap == "default_28": + elif cmap_name == "default_28": cmap = palettes.default_28 - elif cmap == "jana_40": + elif cmap_name == "jana_40": cmap = palettes_st.jana_40 - elif cmap == "default": + elif cmap_name == "default": cmap = palettes_st.default else: raise ValueError( diff --git a/stlearn/adds/add_positions.py b/stlearn/adds/add_positions.py index b993a9d3..1ae3f9d6 100644 --- a/stlearn/adds/add_positions.py +++ b/stlearn/adds/add_positions.py @@ -6,8 +6,8 @@ def positions( adata: AnnData, - position_filepath: Path | str = None, - scale_filepath: Path | str = None, + position_filepath: Path | str, + scale_filepath: Path | str, quality: str = "low", copy: bool = False, ) -> AnnData | None: diff --git a/stlearn/classes.py b/stlearn/classes.py index 2c9026e9..3068a55b 100644 --- a/stlearn/classes.py +++ b/stlearn/classes.py @@ -26,13 +26,13 @@ def __init__( adata: AnnData, basis: str = "spatial", img: np.ndarray | None = None, - img_key: str | None | Empty = _empty, - library_id: str | None = _empty, - crop_coord: bool | None = True, - bw: bool | None = False, + img_key: str | Empty = _empty, + library_id: str | None = None, + crop_coord: bool = True, + bw: bool = False, scale_factor: float | None = None, spot_size: float | None = None, - use_raw: bool | None = False, + use_raw: bool = False, **kwargs, ): diff --git a/stlearn/embedding/umap.py b/stlearn/embedding/umap.py index 9b375a80..db59f5ad 100644 --- a/stlearn/embedding/umap.py +++ b/stlearn/embedding/umap.py @@ -74,3 +74,5 @@ def run_umap( ) print("UMAP is done! Generated in adata.obsm['X_umap'] nad adata.uns['umap']") + + return adata diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index f046e973..9007e83f 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -1,4 +1,5 @@ """Helper functions for cci_plot.py.""" +from typing import List, Tuple, Optional import matplotlib import matplotlib.cm as cm @@ -221,18 +222,18 @@ def rank_scatter( def add_arrows( adata: AnnData, - l_expr: np.array, - r_expr: np.array, + l_expr: np.ndarray, + r_expr: np.ndarray, min_expr: float, - sig_bool: np.array, + sig_bool: np.ndarray, fig, ax: Axes, use_label: str | None, int_df: pd.DataFrame | None, - head_width=4, - width=0.001, - arrow_cmap=None, - arrow_vmax=None, + head_width: float = 4, + width: float = 0.001, + arrow_cmap: str | None =None, + arrow_vmax: float | None =None, ): """ Adds arrows to the current plot for significant spots to neighbours \ which is interacting with. @@ -273,7 +274,7 @@ def add_arrows( interact_bool = int_df.values > 0 # Subsetting to only significant CCI # - edges_sub = [[], []] # forward, reverse + edges_sub: List[List[Tuple[str, str]]] = [[], []] # forward, reverse # ints_2 = np.zeros(int_df.shape) # Just for debugging make sure edge # list re-capitulates edge-counts. for i, edges in enumerate([forward_edges, reverse_edges]): @@ -299,7 +300,7 @@ def add_arrows( # If cmap specified, colour arrows by average LR expression on edge # if arrow_cmap is not None: - edges_means = [[], []] + edges_means: List[List[float]] = [[], []] all_means = [] for i, edges in enumerate([forward_edges, reverse_edges]): for j, edge in enumerate(edges): @@ -320,7 +321,7 @@ def add_arrows( scalar_map = cm.ScalarMappable(norm=c_norm, cmap=cmap) # Determining the edge colors # - edges_colors = [[], []] + edges_colors: List[List[Tuple[float, float, float, float]]] = [[], []] for i, edges in enumerate([forward_edges, reverse_edges]): for j, edge in enumerate(edges): color_val = scalar_map.to_rgba(edges_means[i][j]) @@ -336,7 +337,7 @@ def add_arrows( axc = fig.add_axes(cax) else: - edges_colors = [None, None] + edges_colors = [[], []] # Now performing the plotting # # The arrows # @@ -378,6 +379,8 @@ def add_arrows_by_edges( edge_colors=None, axc=None, ): + if edge_colors is None: + edge_colors = [] """Adds the arrows using an edge list.""" for i, edge in enumerate(edges): # cols = ["imagecol", "imagerow"] @@ -394,7 +397,7 @@ def add_arrows_by_edges( x1, y1 = adata.obsm["spatial"][edge0_index, :] * scale_factor x2, y2 = adata.obsm["spatial"][edge1_index, :] * scale_factor dx, dy = (x2 - x1) * 0.75, (y2 - y1) * 0.75 - arrow_color = "k" if edge_colors is None else edge_colors[i] + arrow_color = "k" if len(edge_colors) == 0 else edge_colors[i] ax.arrow( x1, diff --git a/stlearn/plotting/mask_plot.py b/stlearn/plotting/mask_plot.py index e3e13ed4..05ee3f9f 100644 --- a/stlearn/plotting/mask_plot.py +++ b/stlearn/plotting/mask_plot.py @@ -5,17 +5,17 @@ def plot_mask( adata: AnnData, - library_id: str = None, + library_id: str | None = None, show_spot: bool = True, spot_alpha: float = 1.0, - cmap: str = "vega_20_scanpy", + cmap_name: str = "vega_20_scanpy", tissue_alpha: float = 1.0, mask_alpha: float = 0.5, spot_size: float | int = 6.5, show_legend: bool = True, name: str = "mask_plot", dpi: int = 150, - output: str = None, + output: str | None = None, show_axis: bool = False, show_plot: bool = True, ) -> AnnData | None: @@ -60,17 +60,17 @@ def plot_mask( from stlearn.plotting import palettes_st - if cmap == "vega_10_scanpy": + if cmap_name == "vega_10_scanpy": cmap = palettes.vega_10_scanpy - elif cmap == "vega_20_scanpy": + elif cmap_name == "vega_20_scanpy": cmap = palettes.vega_20_scanpy - elif cmap == "default_102": + elif cmap_name == "default_102": cmap = palettes.default_102 - elif cmap == "default_28": + elif cmap_name == "default_28": cmap = palettes.default_28 - elif cmap == "jana_40": + elif cmap_name == "jana_40": cmap = palettes_st.jana_40 - elif cmap == "default": + elif cmap_name == "default": cmap = palettes_st.default else: raise ValueError( @@ -172,3 +172,5 @@ def plot_mask( if show_plot: plt.show() + + return adata diff --git a/stlearn/plotting/trajectory/DE_transition_plot.py b/stlearn/plotting/trajectory/DE_transition_plot.py index 55275f6b..5f7b5147 100644 --- a/stlearn/plotting/trajectory/DE_transition_plot.py +++ b/stlearn/plotting/trajectory/DE_transition_plot.py @@ -8,9 +8,9 @@ def DE_transition_plot( adata: AnnData, top_genes: int = 10, font_size: int = 6, - name: str = None, + name: str | None = None, dpi: int = 150, - output: str = None, + output: str | None = None, ) -> AnnData | None: """\ Differential expression between transition markers. @@ -239,3 +239,5 @@ def DE_transition_plot( if output is not None: if name is not None: plt.savefig(output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0) + + return adata \ No newline at end of file diff --git a/stlearn/plotting/trajectory/local_plot.py b/stlearn/plotting/trajectory/local_plot.py index de9a9bca..4fa1644b 100644 --- a/stlearn/plotting/trajectory/local_plot.py +++ b/stlearn/plotting/trajectory/local_plot.py @@ -7,8 +7,8 @@ def local_plot( adata: AnnData, + use_cluster: int, use_label: str = "louvain", - use_cluster: int = None, reverse: bool = False, cluster: int = 0, data_alpha: float = 1.0, @@ -18,9 +18,9 @@ def local_plot( show_color_bar: bool = True, show_axis: bool = False, show_plot: bool = True, - name: str = None, + name: str | None = None, dpi: int = 150, - output: str = None, + output: str | None = None, copy: bool = False, ) -> AnnData | None: """\ @@ -185,6 +185,8 @@ def local_plot( name = use_label fig.savefig(output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0) + return adata + def calculate_y(m): import math diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index bbe1b098..b6d3a167 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -9,10 +9,10 @@ def pseudotime_plot( adata: AnnData, - library_id: str = None, + library_id: str | None = None, use_label: str = "louvain", pseudotime_key: str = "pseudotime_key", - list_clusters: str | list = None, + list_clusters: str | list | None = None, cell_alpha: float = 1.0, image_alpha: float = 1.0, edge_alpha: float = 0.8, @@ -29,8 +29,8 @@ def pseudotime_plot( cropped: bool = True, margin: int = 100, dpi: int = 150, - output: str = None, - name: str = None, + output: str | None = None, + name: str | None = None, copy: bool = False, ax=None, ) -> AnnData | None: @@ -74,7 +74,9 @@ def pseudotime_plot( dpi DPI of the output figure. output - Save the figure as file or not. + The output folder of the plot. + name + The filename of the plot. copy Return a copy instead of writing to adata. Returns @@ -263,11 +265,13 @@ def pseudotime_plot( a.set_ylim(a.get_ylim()[::-1]) # plt.gca().invert_yaxis() - if output is not None: + if output is not None and name is not None: fig.savefig(output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0) if show_plot: plt.show() + return adata + # get name of cluster by subcluster def get_cluster(search, dictionary): diff --git a/stlearn/plotting/trajectory/transition_markers_plot.py b/stlearn/plotting/trajectory/transition_markers_plot.py index 6d2bf260..cf93e93d 100644 --- a/stlearn/plotting/trajectory/transition_markers_plot.py +++ b/stlearn/plotting/trajectory/transition_markers_plot.py @@ -6,11 +6,11 @@ def transition_markers_plot( adata: AnnData, + trajectory: str, top_genes: int = 10, - trajectory: str = None, dpi: int = 150, - output: str = None, - name: str = None, + output: str | None = None, + name: str | None = None, ) -> AnnData | None: """\ Plot transition marker. @@ -19,10 +19,10 @@ def transition_markers_plot( ---------- adata Annotated data matrix. - top_genes - Top genes users want to display in the plot. trajectory Name of a clade/branch user wants to plot transition markers. + top_genes + Top genes users want to display in the plot. dpi The resolution of the plot. output @@ -34,10 +34,8 @@ def transition_markers_plot( Anndata """ - if trajectory is None: - raise ValueError("Please input the trajectory name!") if trajectory not in adata.uns: - raise ValueError("Please input the right trajectory name!") + raise ValueError("Please input the right trajectory name - not found in adata.uns!") pos = ( adata.uns[trajectory][adata.uns[trajectory]["score"] >= 0] @@ -146,7 +144,9 @@ def transition_markers_plot( if name is None: name = trajectory - if output is not None: + if output is not None and name is not None: fig.savefig(output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0) plt.show() + + return adata diff --git a/stlearn/preprocessing/graph.py b/stlearn/preprocessing/graph.py index 4330abbd..1cfb2d05 100644 --- a/stlearn/preprocessing/graph.py +++ b/stlearn/preprocessing/graph.py @@ -113,3 +113,5 @@ def neighbors( ) print("Created k-Nearest-Neighbor graph in adata.uns['neighbors'] ") + + return adata diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index f604e4fe..35638614 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -13,7 +13,7 @@ def normalize_total( exclude_highly_expressed: bool = False, max_fraction: float = 0.05, key_added: str | None = None, - layers: Literal["all"] | Iterable[str] = None, + layers: Literal["all"] | Iterable[str] | None = None, layer_norm: str | None = None, inplace: bool = True, ) -> dict[str, np.ndarray] | None: @@ -71,7 +71,7 @@ def normalize_total( `adata.X` and `adata.layers`, depending on `inplace`. """ - scanpy.pp.normalize_total( + t = scanpy.pp.normalize_total( adata, target_sum=target_sum, exclude_highly_expressed=exclude_highly_expressed, @@ -83,3 +83,5 @@ def normalize_total( ) print("Normalization step is finished in adata.X") + + return t diff --git a/stlearn/spatials/SME/_weighting_matrix.py b/stlearn/spatials/SME/_weighting_matrix.py index fcaa2f78..dfc10727 100644 --- a/stlearn/spatials/SME/_weighting_matrix.py +++ b/stlearn/spatials/SME/_weighting_matrix.py @@ -27,6 +27,7 @@ def calculate_weight_matrix( from sklearn.linear_model import LinearRegression + rate: float if platform == "Visium": img_row = adata.obs["imagerow"] img_col = adata.obs["imagecol"] @@ -102,11 +103,12 @@ def calculate_weight_matrix( adata.uns["gene_expression_correlation"] * adata.uns["morphological_distance"] ) + return adata def impute_neighbour( adata: AnnData, - count_embed: np.ndarray | None = None, + count_embed: np.ndarray, weights: _WEIGHTING_MATRIX = "weights_matrix_all", copy: bool = False, ) -> AnnData | None: diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index 1a9c2e3e..ce15c836 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -8,7 +8,7 @@ def localization( adata: AnnData, use_label: str = "louvain", - eps: int = 20, + eps: float = 20, min_samples: int = 0, copy: bool = False, ) -> AnnData | None: diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatials/trajectory/global_level.py index 0cf96464..faf91c92 100644 --- a/stlearn/spatials/trajectory/global_level.py +++ b/stlearn/spatials/trajectory/global_level.py @@ -1,3 +1,4 @@ +import networkx import networkx as nx import numpy as np from anndata import AnnData @@ -8,15 +9,15 @@ def global_level( adata: AnnData, + list_clusters: list[str], + w: float, use_label: str = "louvain", use_rep: str = "X_pca", n_dims: int = 40, - list_clusters: list = [], return_graph: bool = False, - w: float = None, verbose: bool = True, copy: bool = False, -) -> AnnData | None: +) -> networkx.Graph | None: """\ Perform global sptial trajectory inference. @@ -26,12 +27,12 @@ def global_level( Annotated data matrix. list_clusters Setup a list of cluster to perform pseudo-space-time + w + Pseudo-spatio-temporal distance weight (balance between spatial effect and DPT) use_label Use label result of cluster method. return_graph Return PTS graph - w - Pseudo-spatio-temporal distance weight (balance between spatial effect and DPT) copy Return a copy instead of writing to adata. Returns @@ -110,7 +111,7 @@ def global_level( centroid_dict = adata.uns["centroid_dict"] centroid_dict = {int(key): centroid_dict[key] for key in centroid_dict} - H_sub = H.edge_subgraph(edge_list) + H_sub: networkx.Graph = H.edge_subgraph(edge_list) if not nx.is_connected(H_sub.to_undirected()): raise ValueError( "The chosen clusters are not available to construct the spatial " @@ -176,6 +177,8 @@ def global_level( if return_graph: return H_sub + else: + return None # Global level PTS diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index 035556b4..6eb3f3c2 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -7,7 +7,7 @@ def pseudotime( adata: AnnData, - use_label: str = None, + use_label: str | None = None, eps: float = 20, n_neighbors: int = 25, use_rep: str = "X_pca", diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index ba52ae47..89a74a3c 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -5,19 +5,9 @@ from anndata import AnnData from numpy.random.mtrand import RandomState from scipy.sparse import spmatrix - -from stlearn._compat import Literal - -try: - from louvain.VertexPartition import MutableVertexPartition -except ImportError: - - class MutableVertexPartition: - pass - - MutableVertexPartition.__module__ = "louvain.VertexPartition" import scanpy - +from stlearn._compat import Literal +from louvain.VertexPartition import MutableVertexPartition def louvain( adata: AnnData, @@ -106,3 +96,5 @@ def louvain( print( "Louvain cluster is done! The labels are stored in adata.obs['%s']" % key_added ) + + return adata diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index 6a22672d..1758f154 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -194,7 +194,7 @@ def run( adata: AnnData, lrs: np.ndarray, min_spots: int = 10, - distance: int | None = None, + distance: float | None = None, n_pairs: int = 1000, n_cpus: int | None = None, use_label: str | None = None, diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tools/microenv/cci/base.py index 6a5b5259..c72b2db3 100644 --- a/stlearn/tools/microenv/cci/base.py +++ b/stlearn/tools/microenv/cci/base.py @@ -12,9 +12,9 @@ def lr( adata: AnnData, use_lr: str = "cci_lr", - distance: float = None, + distance: float | None = None, verbose: bool = True, - neighbours: list = None, + neighbours: list | None = None, fast: bool = True, ) -> AnnData: """Calculate the proportion of known ligand-receptor co-expression among the @@ -27,7 +27,7 @@ def lr( object to keep the result (default: adata.uns['cci_lr']) distance: float Distance to determine the neighbours (default: closest), distance=0 means - within spot + within spot. If distance is None gets it from adata.uns["spatial"] neighbours: list List of the neighbours for each spot, if None then computed. Useful for speeding up function. @@ -71,7 +71,7 @@ def lr( # return adata -def calc_distance(adata: AnnData, distance: float | None): +def calc_distance(adata: AnnData, distance: float | None) -> float: """Automatically calculate distance if not given, won't overwrite \ distance=0 which is within-spot. Parameters @@ -85,7 +85,7 @@ def calc_distance(adata: AnnData, distance: float | None): Returns ------- distance: float - The automatically calcualted distance (or inputted distance) + The automatically calculate distance (or inputted distance) """ if not distance and distance != 0: # for arranged-spots From 7e1407a8fa6b6ac84ed165ea96743b00e515b77e Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 16:06:10 +1000 Subject: [PATCH 040/241] Fix up missing exports. --- stlearn/datasets.py | 5 ++ stlearn/em.py | 15 ++++++ stlearn/pl.py | 55 +++++++++++++++++++++ stlearn/tools/microenv/cci/base_grouping.py | 2 - stlearn/tools/microenv/cci/het.py | 2 +- 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/stlearn/datasets.py b/stlearn/datasets.py index e69de29b..a8c0721e 100644 --- a/stlearn/datasets.py +++ b/stlearn/datasets.py @@ -0,0 +1,5 @@ +from ._datasets._datasets import example_bcba + +__all__ = [ + "example_bcba", +] diff --git a/stlearn/em.py b/stlearn/em.py index d16c7bec..bfac0db9 100644 --- a/stlearn/em.py +++ b/stlearn/em.py @@ -1 +1,16 @@ # from .embedding.scvi import run_ldvae +from .embedding.pca import run_pca +from .embedding.umap import run_umap +from .embedding.ica import run_ica + +# from .embedding.scvi import run_ldvae +from .embedding.fa import run_fa +from .embedding.diffmap import run_diffmap + +__all__ = [ + "run_pca", + "run_umap", + "run_ica", + "run_fa", + "run_diffmap", +] \ No newline at end of file diff --git a/stlearn/pl.py b/stlearn/pl.py index a41cb8d6..56baff5c 100644 --- a/stlearn/pl.py +++ b/stlearn/pl.py @@ -1 +1,56 @@ # from .plotting.cci_plot import het_plot_interactive +from .plotting import trajectory +from .plotting.QC_plot import QC_plot +from .plotting.cci_plot import ( + ccinet_plot, + cci_map, + lr_cci_map, + lr_chord_plot, + cci_check, +) +from .plotting.cci_plot import grid_plot +from .plotting.cci_plot import het_plot +from .plotting.cci_plot import lr_diagnostics, lr_n_spots, lr_summary, lr_go +from .plotting.cci_plot import lr_plot, lr_result_plot +# from .plotting.cci_plot import het_plot_interactive +from .plotting.cci_plot import lr_plot_interactive, spatialcci_plot_interactive +from .plotting.cluster_plot import cluster_plot +from .plotting.cluster_plot import cluster_plot_interactive +from .plotting.deconvolution_plot import deconvolution_plot +from .plotting.feat_plot import feat_plot +from .plotting.gene_plot import gene_plot +from .plotting.gene_plot import gene_plot_interactive +from .plotting.mask_plot import plot_mask +from .plotting.non_spatial_plot import non_spatial_plot +from .plotting.stack_3d_plot import stack_3d_plot +from .plotting.subcluster_plot import subcluster_plot + +__all__ = [ + "gene_plot", + "gene_plot_interactive", + "feat_plot", + "cluster_plot", + "cluster_plot_interactive", + "subcluster_plot", + "non_spatial_plot", + "deconvolution_plot", + "stack_3d_plot", + "trajectory", + "QC_plot", + "het_plot", + "lr_plot_interactive", + "spatialcci_plot_interactive", + "grid_plot", + "lr_diagnostics", + "lr_n_spots", + "lr_summary", + "lr_go", + "lr_plot", + "lr_result_plot", + "ccinet_plot", + "cci_map", + "lr_cci_map", + "lr_chord_plot", + "cci_check", + "plot_mask", +] diff --git a/stlearn/tools/microenv/cci/base_grouping.py b/stlearn/tools/microenv/cci/base_grouping.py index 24201a71..a980ae9d 100644 --- a/stlearn/tools/microenv/cci/base_grouping.py +++ b/stlearn/tools/microenv/cci/base_grouping.py @@ -9,10 +9,8 @@ from anndata import AnnData from sklearn.cluster import DBSCAN, AgglomerativeClustering from tqdm import tqdm - from stlearn.pl import het_plot - def get_hotspots( adata: AnnData, lr_scores: np.ndarray, diff --git a/stlearn/tools/microenv/cci/het.py b/stlearn/tools/microenv/cci/het.py index 54232564..1d70c4e0 100644 --- a/stlearn/tools/microenv/cci/het.py +++ b/stlearn/tools/microenv/cci/het.py @@ -448,7 +448,7 @@ def count_grid( adata: AnnData, num_row: int = 30, num_col: int = 30, - use_label: str = None, + use_label: str | None = None, use_het: str = "cci_het_grid", radius: int = 1, verbose: bool = True, From 127d022b4aa7abdbc39dceb3ddf46a00fbe60cec Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 5 Jun 2025 16:16:04 +1000 Subject: [PATCH 041/241] Fix up types. --- stlearn/__main__.py | 4 ++-- stlearn/adds/add_labels.py | 2 +- stlearn/tools/microenv/cci/het.py | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/stlearn/__main__.py b/stlearn/__main__.py index 4687bf58..3802ae27 100644 --- a/stlearn/__main__.py +++ b/stlearn/__main__.py @@ -3,7 +3,7 @@ """Package entry point.""" -from stlearn.app import main +from stlearn.app import cli if __name__ == "__main__": # pragma: no cover - main() + cli.main() diff --git a/stlearn/adds/add_labels.py b/stlearn/adds/add_labels.py index 5b0875f7..76d69c7c 100644 --- a/stlearn/adds/add_labels.py +++ b/stlearn/adds/add_labels.py @@ -6,7 +6,7 @@ def labels( adata: AnnData, - label_filepath: str = None, + label_filepath: str, index_col: int = 0, use_label: str = None, sep: str = "\t", diff --git a/stlearn/tools/microenv/cci/het.py b/stlearn/tools/microenv/cci/het.py index 1d70c4e0..60e6e1bb 100644 --- a/stlearn/tools/microenv/cci/het.py +++ b/stlearn/tools/microenv/cci/het.py @@ -1,3 +1,5 @@ +from typing import Iterable + import numpy as np import pandas as pd import scipy.spatial as spatial @@ -414,7 +416,7 @@ def create_grids(adata: AnnData, num_row: int, num_col: int, radius: int = 1): grids, neighbours = [], [] # generate grids from top to bottom and left to right for n in range(num_row * num_col): - neighbour = [] + neighbour: Iterable[float] x = min_x + n // num_row * width # left side y = min_y + n % num_row * height # upper side grids.append([x, y]) From f3d70d6365f527350a244f0a097d2e98b79ac198 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 6 Jun 2025 13:48:18 +1000 Subject: [PATCH 042/241] Fixing types. --- stlearn/adds/add_deconvolution.py | 4 ++ stlearn/adds/add_image.py | 3 +- stlearn/adds/add_labels.py | 4 +- stlearn/adds/add_loupe_clusters.py | 4 ++ stlearn/adds/add_lr.py | 1 + stlearn/adds/add_mask.py | 8 +-- stlearn/adds/add_positions.py | 2 + stlearn/adds/annotation.py | 3 +- stlearn/adds/parsing.py | 6 +- stlearn/classes.py | 2 +- stlearn/embedding/diffmap.py | 4 +- stlearn/embedding/fa.py | 4 +- stlearn/embedding/ica.py | 8 +-- stlearn/embedding/pca.py | 6 +- stlearn/embedding/umap.py | 2 +- .../image_preprocessing/feature_extractor.py | 3 + stlearn/image_preprocessing/image_tiling.py | 2 + stlearn/plotting/QC_plot.py | 8 +-- stlearn/plotting/deconvolution_plot.py | 42 +++++------- stlearn/plotting/non_spatial_plot.py | 2 +- stlearn/plotting/stack_3d_plot.py | 2 +- .../plotting/trajectory/check_trajectory.py | 14 ++-- stlearn/preprocessing/filter_genes.py | 4 +- stlearn/preprocessing/log_scale.py | 8 +-- stlearn/spatials/SME/impute.py | 4 ++ stlearn/spatials/SME/normalize.py | 2 + stlearn/spatials/clustering/localization.py | 2 + stlearn/spatials/morphology/adjust.py | 2 + stlearn/spatials/smooth/disk.py | 3 + stlearn/spatials/trajectory/global_level.py | 8 +-- stlearn/spatials/trajectory/local_level.py | 17 +++-- stlearn/spatials/trajectory/utils.py | 13 ---- stlearn/tools/clustering/kmeans.py | 2 +- stlearn/utils.py | 67 ++++++++++++++----- stlearn/wrapper/read.py | 6 +- 35 files changed, 165 insertions(+), 107 deletions(-) diff --git a/stlearn/adds/add_deconvolution.py b/stlearn/adds/add_deconvolution.py index d169b8ed..5d892dda 100644 --- a/stlearn/adds/add_deconvolution.py +++ b/stlearn/adds/add_deconvolution.py @@ -27,7 +27,11 @@ def add_deconvolution( The annotation of cluster results. """ + adata = adata.copy() if copy else adata + label = pd.read_csv(annotation_path, index_col=0) label = label[adata.obs_names] adata.obsm["deconvolution"] = label[adata.obs.index].T + + return adata diff --git a/stlearn/adds/add_image.py b/stlearn/adds/add_image.py index 0d07b3d3..20376ece 100644 --- a/stlearn/adds/add_image.py +++ b/stlearn/adds/add_image.py @@ -45,6 +45,7 @@ def image( **tissue_img** : `adata.uns` field Array format of image, saving by Pillow package. """ + adata = adata.copy() if copy else adata if imgpath is not None and os.path.isfile(imgpath): try: @@ -69,8 +70,6 @@ def image( adata.obs[["imagecol", "imagerow"]] = adata.obsm["spatial"] * scale print("Added tissue image to the object!") - - return adata if copy else None except: raise ValueError( f"""\ diff --git a/stlearn/adds/add_labels.py b/stlearn/adds/add_labels.py index 76d69c7c..d11cad49 100644 --- a/stlearn/adds/add_labels.py +++ b/stlearn/adds/add_labels.py @@ -8,7 +8,7 @@ def labels( adata: AnnData, label_filepath: str, index_col: int = 0, - use_label: str = None, + use_label: str | None = None, sep: str = "\t", copy: bool = False, ) -> AnnData | None: @@ -35,6 +35,8 @@ def labels( The data object that L-R added into """ + adata = adata.copy() if copy else adata + labels = pd.read_csv(label_filepath, index_col=index_col, sep=sep) uns_key = "label_transfer" if use_label is None else use_label adata.uns[uns_key] = labels.drop(["predicted.id", "prediction.score.max"], axis=1) diff --git a/stlearn/adds/add_loupe_clusters.py b/stlearn/adds/add_loupe_clusters.py index 4d1baef2..a85b15cf 100644 --- a/stlearn/adds/add_loupe_clusters.py +++ b/stlearn/adds/add_loupe_clusters.py @@ -34,9 +34,13 @@ def add_loupe_clusters( The annotation of cluster results. """ + adata = adata.copy() if copy else adata + label = pd.read_csv(loupe_path) adata.obs[key_add] = pd.Categorical( values=np.array(label[key_add]).astype("U"), categories=natsorted(label[key_add].unique().astype("U")), ) + + return adata if copy else None \ No newline at end of file diff --git a/stlearn/adds/add_lr.py b/stlearn/adds/add_lr.py index 78df7424..d40d11a8 100644 --- a/stlearn/adds/add_lr.py +++ b/stlearn/adds/add_lr.py @@ -29,6 +29,7 @@ def lr( adata: AnnData The data object that L-R added into """ + adata = adata.copy() if copy else adata if source == "cellphonedb": cpdb = pd.read_csv(db_filepath, sep=sep) diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index dd086217..680885f8 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -32,6 +32,8 @@ def add_mask( **mask_image** : `adata.uns` field Array format of image, saving by Pillow package. """ + adata = adata.copy() if copy else adata + try: library_id = list(adata.uns["spatial"].keys())[0] quality = adata.uns["spatial"][library_id]["use_quality"] @@ -58,8 +60,6 @@ def add_mask( adata.uns["mask_image"][library_id][key][quality] = img print("Added tissue mask to the object!") - - return adata if copy else None except: raise ValueError( f"""\ @@ -105,9 +105,10 @@ def apply_mask( Array format of image, saving by Pillow package. """ from scanpy.plotting import palettes - from stlearn.plotting import palettes_st + adata = adata.copy() if copy else adata + if cmap_name == "vega_10_scanpy": cmap = palettes.vega_10_scanpy elif cmap_name == "vega_20_scanpy": @@ -126,7 +127,6 @@ def apply_mask( ) cmaps = matplotlib.colors.LinearSegmentedColormap.from_list("", cmap) - cmap_ = plt.cm.get_cmap(cmaps) try: diff --git a/stlearn/adds/add_positions.py b/stlearn/adds/add_positions.py index 1ae3f9d6..7b4c3cb7 100644 --- a/stlearn/adds/add_positions.py +++ b/stlearn/adds/add_positions.py @@ -33,6 +33,8 @@ def positions( Spatial information of the tissue image. """ + adata = adata.copy() if copy else adata + tissue_positions = pd.read_csv(position_filepath, header=None) tissue_positions.columns = [ "barcode", diff --git a/stlearn/adds/annotation.py b/stlearn/adds/annotation.py index 809c0cea..8f5df9db 100644 --- a/stlearn/adds/annotation.py +++ b/stlearn/adds/annotation.py @@ -26,10 +26,11 @@ def annotation( **[cluster method name]_anno** : `adata.obs` field The annotation of cluster results. """ - if label_list is None: raise ValueError("Please give the label list!") + adata = adata.copy() if copy else adata + if len(label_list) != len(adata.obs[use_label].unique()): raise ValueError("Please give the correct number of label list!") diff --git a/stlearn/adds/parsing.py b/stlearn/adds/parsing.py index 282ed38d..4b824c59 100644 --- a/stlearn/adds/parsing.py +++ b/stlearn/adds/parsing.py @@ -1,3 +1,4 @@ +from os import PathLike from pathlib import Path import numpy as np @@ -6,7 +7,7 @@ def parsing( adata: AnnData, - coordinates_file: Path | str | None, + coordinates_file: int | str | bytes | PathLike[str] | PathLike[bytes], copy: bool = True, ) -> AnnData | None: """\ @@ -48,6 +49,8 @@ def parsing( "the coordinates file only contains 4 columns\n" ) + adata = adata.copy() if copy else adata + counts_table = adata.to_df() new_index_values = list() @@ -76,7 +79,6 @@ def parsing( adata.obs["imagecol"] = imgcol adata.obs["imagerow"] = imgrow - adata.obsm["spatial"] = np.c_[[imgcol, imgrow]].reshape(-1, 2) return adata if copy else None diff --git a/stlearn/classes.py b/stlearn/classes.py index 3068a55b..f441661b 100644 --- a/stlearn/classes.py +++ b/stlearn/classes.py @@ -26,7 +26,7 @@ def __init__( adata: AnnData, basis: str = "spatial", img: np.ndarray | None = None, - img_key: str | Empty = _empty, + img_key: str | None | Empty = _empty, library_id: str | None = None, crop_coord: bool = True, bw: bool = False, diff --git a/stlearn/embedding/diffmap.py b/stlearn/embedding/diffmap.py index 93338007..fb309d9e 100644 --- a/stlearn/embedding/diffmap.py +++ b/stlearn/embedding/diffmap.py @@ -34,11 +34,11 @@ def run_diffmap(adata: AnnData, n_comps: int = 15, copy: bool = False): Eigenvalues of transition matrix. """ - scanpy.tl.diffmap(adata, n_comps=n_comps, copy=copy) + adata = scanpy.tl.diffmap(adata, n_comps=n_comps, copy=copy) print( "Diffusion Map is done! Generated in adata.obsm['X_diffmap'] and " + "adata.uns['diffmap_evals']" ) - return adata if copy else None + return adata diff --git a/stlearn/embedding/fa.py b/stlearn/embedding/fa.py index b982c3d8..953ff96a 100644 --- a/stlearn/embedding/fa.py +++ b/stlearn/embedding/fa.py @@ -11,7 +11,7 @@ def run_fa( svd_method: str = "randomized", iterated_power: int = 3, random_state: int = 2108, - use_data: str = None, + use_data: str | None = None, copy: bool = False, ) -> AnnData | None: """\ @@ -69,6 +69,8 @@ def run_fa( Factor analysis representation of data. """ + adata = adata.copy() if copy else adata + if use_data is None: if issparse(adata.X): matrix = adata.X.toarray() diff --git a/stlearn/embedding/ica.py b/stlearn/embedding/ica.py index 5b990788..fde64c40 100644 --- a/stlearn/embedding/ica.py +++ b/stlearn/embedding/ica.py @@ -8,7 +8,7 @@ def run_ica( n_factors: int = 20, fun: str = "logcosh", tol: float = 0.0001, - use_data: str = None, + use_data: str | None = None, copy: bool = False, ) -> AnnData | None: """\ @@ -43,21 +43,19 @@ def my_g(x): Independent Component Analysis representation of data. """ + adata = adata.copy() if copy else adata + if use_data is None: if issparse(adata.X): matrix = adata.X.toarray() else: matrix = adata.X - else: matrix = adata.obsm[use_data].values ica = FastICA(n_components=n_factors, fun=fun, tol=tol) - latent = ica.fit_transform(matrix) - adata.obsm["X_ica"] = latent - adata.uns["ica"] = {"params": {"n_factors": n_factors, "fun": fun, "tol": tol}} print( diff --git a/stlearn/embedding/pca.py b/stlearn/embedding/pca.py index 22ae94fb..8870994e 100644 --- a/stlearn/embedding/pca.py +++ b/stlearn/embedding/pca.py @@ -17,7 +17,7 @@ def run_pca( copy: bool = False, chunked: bool = False, chunk_size: int | None = None, -) -> AnnData | np.ndarray | spmatrix: +) -> AnnData | None: """\ Wrap function scanpy.pp.pca Principal component analysis [Pedregosa11]_. @@ -83,7 +83,7 @@ def run_pca( covariance matrix. """ - scanpy.pp.pca( + adata = scanpy.pp.pca( data, n_comps=n_comps, zero_center=zero_center, @@ -101,3 +101,5 @@ def run_pca( "PCA is done! Generated in adata.obsm['X_pca'], adata.uns['pca'] and " + "adata.varm['PCs']" ) + + return adata diff --git a/stlearn/embedding/umap.py b/stlearn/embedding/umap.py index db59f5ad..aa509979 100644 --- a/stlearn/embedding/umap.py +++ b/stlearn/embedding/umap.py @@ -56,7 +56,7 @@ def run_umap( """ - scanpy.tl.umap( + adata = scanpy.tl.umap( adata, min_dist=min_dist, spread=spread, diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index 75e3a2a2..dc07b343 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -45,6 +45,9 @@ def extract_feature( **X_morphology** : `adata.obsm` field Dimension reduced latent morphological features. """ + + adata = adata.copy() if copy else adata + feature_dfs = [] model = Model(cnn_base) diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index ee338816..098cc58a 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -45,6 +45,8 @@ def tiling( Saved path for each spot image tiles """ + adata = adata.copy() if copy else adata + if library_id is None: library_id = list(adata.uns["spatial"].keys())[0] diff --git a/stlearn/plotting/QC_plot.py b/stlearn/plotting/QC_plot.py index ebe0faa6..a2224857 100644 --- a/stlearn/plotting/QC_plot.py +++ b/stlearn/plotting/QC_plot.py @@ -5,8 +5,8 @@ def QC_plot( adata: AnnData, - library_id: str = None, - name: str = None, + name: str, + library_id: str | None = None, data_alpha: float = 0.8, tissue_alpha: float = 1.0, cmap: str = "Spectral_r", @@ -17,8 +17,8 @@ def QC_plot( cropped: bool = True, margin: int = 100, dpi: int = 150, - output: str = None, -) -> AnnData | None: + output: str | None = None, +) -> None: """\ QC plot for sptial transcriptomics data. diff --git a/stlearn/plotting/deconvolution_plot.py b/stlearn/plotting/deconvolution_plot.py index f41a9f8d..571bd370 100644 --- a/stlearn/plotting/deconvolution_plot.py +++ b/stlearn/plotting/deconvolution_plot.py @@ -1,4 +1,5 @@ import matplotlib as mpl +import matplotlib.colors as mcolors import matplotlib.pyplot as plt import numpy as np from anndata import AnnData @@ -6,30 +7,27 @@ def deconvolution_plot( adata: AnnData, - library_id: str = None, + library_id: str | None = None, use_label: str = "louvain", - cluster: [int, str] = None, - celltype: str = None, + cluster: int | str | None = None, + celltype: str | None = None, celltype_threshold: float = 0, data_alpha: float = 1.0, threshold: float = 0.0, cmap: str = "tab20", - colors: list = None, # The colors to use for each label... - tissue_alpha: float = 1.0, - title: str = None, + colors: list[tuple[float, float, float, float]] | None = None, # The colors to use for each label... spot_size: float | int = 10, show_axis: bool = False, show_legend: bool = True, show_donut: bool = True, cropped: bool = True, margin: int = 100, - name: str = None, + name: str | None = None, dpi: int = 150, - output: str = None, - copy: bool = False, + output: str | None = None, figsize: tuple = (6.4, 4.8), show=True, -) -> AnnData | None: +) -> None: """\ Clustering plot for sptial transcriptomics data. Also, it has a function to display trajectory inference. @@ -42,8 +40,8 @@ def deconvolution_plot( Library id stored in AnnData. use_label Use label result of cluster method. - list_cluster - Choose set of clusters that will display in the plot. + cluster + Choose a cluster (in adata.obs[use_label]) that will display in the plot. data_alpha Opacity of the spot. tissue_alpha @@ -100,12 +98,11 @@ def deconvolution_plot( ] label_filter_ = label_filter[base.index] - if colors is None: - color_vals = list(range(0, len(label_filter_), 1)) - my_norm = mpl.colors.Normalize(0, len(label_filter_)) - my_cmap = mpl.cm.get_cmap(cmap, len(color_vals)) - colors = my_cmap.colors + color_vals: list[int] = list(range(0, len(label_filter_), 1)) + my_norm: mcolors.Normalize = mpl.colors.Normalize(0, len(label_filter_)) + my_cmap: mcolors.Colormap = mpl.cm.get_cmap(cmap, len(color_vals)) + colors = [my_cmap(my_norm(i)) for i in color_vals] for i, xy in enumerate(base.values): _ = ax.pie( @@ -125,14 +122,14 @@ def deconvolution_plot( ] if show_donut: - ax_pie = fig.add_axes([0.5, -0.4, 0.03, 0.5]) + ax_pie = fig.add_axes((0.5, -0.4, 0.03, 0.5)) def my_autopct(pct): return ("%1.0f%%" % pct) if pct >= 4 else "" ax_pie.pie( label_filter_.sum(axis=1), - colors=my_cmap.colors, + colors=colors, radius=10, # frame=True, autopct=my_autopct, @@ -143,8 +140,8 @@ def my_autopct(pct): ) if show_legend: - ax_cb = fig.add_axes([0.9, 0.25, 0.03, 0.5], axisbelow=False) - cb = mpl.colorbar.ColorbarBase( + ax_cb = fig.add_axes((0.9, 0.25, 0.03, 0.5), axisbelow=False) + cb = mpl.pyplot.colorbar.ColorbarBase( ax_cb, cmap=my_cmap, norm=my_norm, ticks=color_vals ) @@ -165,11 +162,8 @@ def my_autopct(pct): if cropped: ax.set_xlim(imagecol.min() - margin, imagecol.max() + margin) - ax.set_ylim(imagerow.min() - margin, imagerow.max() + margin) - ax.set_ylim(ax.get_ylim()[::-1]) - # plt.gca().invert_yaxis() if name is None: diff --git a/stlearn/plotting/non_spatial_plot.py b/stlearn/plotting/non_spatial_plot.py index 600f7897..800d5779 100644 --- a/stlearn/plotting/non_spatial_plot.py +++ b/stlearn/plotting/non_spatial_plot.py @@ -6,7 +6,7 @@ def non_spatial_plot( adata: AnnData, use_label: str = "louvain", -) -> AnnData | None: +) -> None: """\ A wrap function to plot all the non-spatial plot from scanpy. diff --git a/stlearn/plotting/stack_3d_plot.py b/stlearn/plotting/stack_3d_plot.py index e13bba29..c958575a 100644 --- a/stlearn/plotting/stack_3d_plot.py +++ b/stlearn/plotting/stack_3d_plot.py @@ -11,7 +11,7 @@ def stack_3d_plot( slide_col="sample_id", use_label=None, gene_symbol=None, -) -> AnnData | None: +) -> None: """\ Clustering plot for spatial transcriptomics data. Also, it has a function to display trajectory inference. diff --git a/stlearn/plotting/trajectory/check_trajectory.py b/stlearn/plotting/trajectory/check_trajectory.py index d9e84f12..587c20e9 100644 --- a/stlearn/plotting/trajectory/check_trajectory.py +++ b/stlearn/plotting/trajectory/check_trajectory.py @@ -6,21 +6,21 @@ def check_trajectory( adata: AnnData, - library_id: str = None, + trajectory: list[int], + library_id: str | None = None, use_label: str = "louvain", basis: str = "umap", pseudotime_key: str = "dpt_pseudotime", - trajectory: list = None, figsize=(10, 4), size_umap: int = 50, - size_spatial: int = 1.5, + size_spatial: float = 1.5, img_key: str = "hires", -) -> AnnData | None: +) -> None: trajectory = np.array(trajectory).astype(int) assert ( trajectory in adata.uns["available_paths"].values() ), "Please choose the right path!" - trajectory = trajectory.astype(str) + trajectory_str = [str(node) for node in trajectory] assert ( pseudotime_key in adata.obs.columns ), "Please run the pseudotime or choose the right one!" @@ -39,7 +39,7 @@ def check_trajectory( ax1 = sc.pl.umap(adata, size=size_umap, show=False, ax=ax1) sc.pl.umap( - adata[adata.obs[use_label].isin(trajectory)], + adata[adata.obs[use_label].isin(trajectory_str)], size=size_umap, color=pseudotime_key, ax=ax1, @@ -55,7 +55,7 @@ def check_trajectory( ax=ax2, ) sc.pl.spatial( - adata[adata.obs[use_label].isin(trajectory)], + adata[adata.obs[use_label].isin(trajectory_str)], size=size_spatial, ax=ax2, color=pseudotime_key, diff --git a/stlearn/preprocessing/filter_genes.py b/stlearn/preprocessing/filter_genes.py index 42cc3d24..71bd4b58 100644 --- a/stlearn/preprocessing/filter_genes.py +++ b/stlearn/preprocessing/filter_genes.py @@ -22,7 +22,7 @@ def filter_genes( `max_counts`, `max_cells` per call. Parameters ---------- - data + adata An annotated data matrix of shape `n_obs` × `n_vars`. Rows correspond to cells and columns to genes. min_counts @@ -47,7 +47,7 @@ def filter_genes( `n_counts` or `n_cells` per gene. """ - scanpy.pp.filter_genes( + return scanpy.pp.filter_genes( adata, min_counts=min_counts, min_cells=min_cells, diff --git a/stlearn/preprocessing/log_scale.py b/stlearn/preprocessing/log_scale.py index 9ebb63e0..0eb1cd1b 100644 --- a/stlearn/preprocessing/log_scale.py +++ b/stlearn/preprocessing/log_scale.py @@ -38,9 +38,9 @@ def log1p( Returns or updates `data`, depending on `copy`. """ - scanpy.pp.log1p(adata, copy=copy, chunked=chunked, chunk_size=chunk_size, base=base) - + result = scanpy.pp.log1p(adata, copy=copy, chunked=chunked, chunk_size=chunk_size, base=base) print("Log transformation step is finished in adata.X") + return result def scale( @@ -75,6 +75,6 @@ def scale( Depending on `copy` returns or updates `adata` with a scaled `adata.X`. """ - scanpy.pp.scale(adata, zero_center=zero_center, max_value=max_value, copy=copy) - + result = scanpy.pp.scale(adata, zero_center=zero_center, max_value=max_value, copy=copy) print("Scale step is finished in adata.X") + return result diff --git a/stlearn/spatials/SME/impute.py b/stlearn/spatials/SME/impute.py index a365b192..2e1b5968 100644 --- a/stlearn/spatials/SME/impute.py +++ b/stlearn/spatials/SME/impute.py @@ -48,6 +48,8 @@ def SME_impute0( ------- Anndata """ + adata = adata.copy() if copy else adata + if use_data == "raw": if isinstance(adata.X, csr_matrix): count_embed = adata.X.toarray() @@ -132,6 +134,8 @@ def pseudo_spot( from sklearn.linear_model import LinearRegression + adata = adata.copy() if copy else adata + if platform == "Visium": img_row = adata.obs["imagerow"] img_col = adata.obs["imagecol"] diff --git a/stlearn/spatials/SME/normalize.py b/stlearn/spatials/SME/normalize.py index 04ef41e4..39f65207 100644 --- a/stlearn/spatials/SME/normalize.py +++ b/stlearn/spatials/SME/normalize.py @@ -42,6 +42,8 @@ def SME_normalize( ------- Anndata """ + adata = adata.copy() if copy else adata + if use_data == "raw": if isinstance(adata.X, csr_matrix): count_embed = adata.X.toarray() diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index ce15c836..efc1774f 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -36,6 +36,8 @@ def localization( Anndata """ + adata = adata.copy() if copy else adata + if "sub_cluster_labels" in adata.obs.columns: adata.obs = adata.obs.drop("sub_cluster_labels", axis=1) diff --git a/stlearn/spatials/morphology/adjust.py b/stlearn/spatials/morphology/adjust.py index 1305a3bb..7aced5f0 100644 --- a/stlearn/spatials/morphology/adjust.py +++ b/stlearn/spatials/morphology/adjust.py @@ -46,6 +46,8 @@ def adjust( **[use_data]_morphology** : `adata.obsm` field Add SME normalised gene expression matrix """ + adata = adata.copy() if copy else adata + if "X_morphology" not in adata.obsm: raise ValueError("Please run the function stlearn.pp.extract_feature") coor = adata.obs[["imagecol", "imagerow"]] diff --git a/stlearn/spatials/smooth/disk.py b/stlearn/spatials/smooth/disk.py index 970d1843..01ff2309 100644 --- a/stlearn/spatials/smooth/disk.py +++ b/stlearn/spatials/smooth/disk.py @@ -11,6 +11,9 @@ def disk( method: str = "mean", copy: bool = False, ) -> AnnData | None: + + adata = adata.copy() if copy else adata + coor = adata.obs[["imagecol", "imagerow"]] count_embed = adata.obsm[use_data] point_tree = spatial.cKDTree(coor) diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatials/trajectory/global_level.py index faf91c92..da0cdf35 100644 --- a/stlearn/spatials/trajectory/global_level.py +++ b/stlearn/spatials/trajectory/global_level.py @@ -16,7 +16,6 @@ def global_level( n_dims: int = 40, return_graph: bool = False, verbose: bool = True, - copy: bool = False, ) -> networkx.Graph | None: """\ Perform global sptial trajectory inference. @@ -33,11 +32,12 @@ def global_level( Use label result of cluster method. return_graph Return PTS graph - copy - Return a copy instead of writing to adata. Returns ------- - Anndata + networkx.Graph: + + adata.uns["PTS_graph"]["graph"]: + adata.uns["PTS_graph"]["node_dict"]: """ assert w <= 1, "w should be in range 0 to 1" diff --git a/stlearn/spatials/trajectory/local_level.py b/stlearn/spatials/trajectory/local_level.py index bd791312..71a155a3 100644 --- a/stlearn/spatials/trajectory/local_level.py +++ b/stlearn/spatials/trajectory/local_level.py @@ -10,8 +10,7 @@ def local_level( w: float = 0.5, return_matrix: bool = False, verbose: bool = True, - copy: bool = False, -) -> AnnData | None: +) -> np.ndarray | None: """\ Perform local sptial trajectory inference (required run pseudotime first). @@ -29,11 +28,15 @@ def local_level( Pseudo-spatio-temporal distance weight (balance between spatial effect and DPT) return_matrix Return PTS matrix for local level - copy - Return a copy instead of writing to adata. Returns ------- - Anndata + np.ndarray: the STDM (spatio-temporal distance matrix) - weighted combination of spatial and temporal distances. + + adata["nonabs_dpt_distance_matrix"]: np.ndarray + Pseudotime distance (difference between values) matrix + + adata["nonabs_dpt_distance_matrix"]: np.ndarray + STDM """ if verbose: print("Start construct trajectory for subcluster " + str(cluster)) @@ -77,5 +80,5 @@ def local_level( if return_matrix: return stdm - - return adata if copy else None + else: + return None \ No newline at end of file diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatials/trajectory/utils.py index 1c06cc51..f4328f50 100644 --- a/stlearn/spatials/trajectory/utils.py +++ b/stlearn/spatials/trajectory/utils.py @@ -568,19 +568,6 @@ def _correlation_test_helper( confidence interval. Each array if of shape ``(n_genes, n_lineages)``. """ - def perm_test_extractor( - res: Sequence[tuple[np.ndarray, np.ndarray]], - ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - pvals, corr_bs = zip(*res) - pvals = np.sum(pvals, axis=0) / float(n_perms) - - corr_bs = np.concatenate(corr_bs, axis=0) - corr_ci_low, corr_ci_high = np.quantile(corr_bs, q=ql, axis=0), np.quantile( - corr_bs, q=qh, axis=0 - ) - - return pvals, corr_ci_low, corr_ci_high - if not (0 <= confidence_level <= 1): raise ValueError( "Expected `confidence_level` to be in interval `[0, 1]`, " diff --git a/stlearn/tools/clustering/kmeans.py b/stlearn/tools/clustering/kmeans.py index 398e4184..822130af 100644 --- a/stlearn/tools/clustering/kmeans.py +++ b/stlearn/tools/clustering/kmeans.py @@ -13,7 +13,7 @@ def kmeans( n_init: int = 10, max_iter: int = 300, tol: float = 0.0001, - random_state: str = None, + random_state: int | np.random.RandomState = None, copy_x: bool = True, algorithm: str = "auto", key_added: str = "kmeans", diff --git a/stlearn/utils.py b/stlearn/utils.py index 581aec49..a4531ea0 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -25,15 +25,17 @@ def _check_spot_size(spatial_data: Mapping | None, spot_size: float | None) -> f Resolve spot_size value. This is a required argument for spatial plots. """ - if spatial_data is None and spot_size is None: + if spot_size is not None: + return spot_size + + if spatial_data is None: raise ValueError( "When .uns['spatial'][library_id] does not exist, spot_size must be " "provided directly." ) - elif spot_size is None: - return spatial_data["scalefactors"]["spot_diameter_fullres"] - else: - return spot_size + + return spatial_data["scalefactors"]["spot_diameter_fullres"] + def _check_scale_factor( @@ -52,7 +54,7 @@ def _check_scale_factor( def _check_spatial_data( uns: Mapping, library_id: Empty | None | str -) -> tuple[str | None, Mapping | None]: +) -> tuple[str | Empty | None, Mapping | None]: """ Given a mapping, try and extract a library id/ mapping with spatial data. Assumes this is `.uns` from how we parse visium data. @@ -83,16 +85,51 @@ def _check_img( ) -> tuple[np.ndarray | None, str | None]: """ Resolve image for spatial plots. + + Parameters + ---------- + img : np.ndarray | None + If given an image will not look for another image and not check to see if it was in spatial_data. + img_key : None | str | Empty + If None - don't find an image. Empty - find best image, or specify with str. + + Returns + ------- + tuple[np.ndarray | None, str | None] + The image found or nothing, str of the key of image found or None if none found. + + """ - if img is None and spatial_data is not None and img_key is _empty: - img_key = next( - (k for k in ["hires", "lowres", "fulres"] if k in spatial_data["images"]), - ) # Throws StopIteration Error if keys not present - if img is None and spatial_data is not None and img_key is not None: - img = spatial_data["images"][img_key] - if bw: - img = np.dot(img[..., :3], [0.2989, 0.5870, 0.1140]) - return img, img_key + + # Return [None, None] if there's no anndata mapping or img + if spatial_data is None and img is None: + return None, None + else: + # Find image and key + new_img_key: str | None = None + new_img: np.ndarray | None = None + + # Return the img if not None and convert the key to Empty -> None if Empty otherwise keep. + if img is not None: + new_img = img + new_img_key = img_key if img_key is not _empty else None + # Find key if empty or use key. + elif spatial_data is not None: + if img_key is _empty: + # Looks for image - or None if not found. + new_img_key = next( + (k for k in ["hires", "lowres", "fulres"] if k in spatial_data["images"]), None + ) + else: + new_img_key = img_key + + if new_img_key is not None: + new_img = spatial_data["images"][new_img_key] + + if new_img is not None and bw: + new_img = np.dot(new_img[..., :3], [0.2989, 0.5870, 0.1140]) + + return new_img, new_img_key def _check_coords( diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index be44ee8d..fbb3fa7a 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -2,7 +2,9 @@ import json import logging as logg +from os import PathLike from pathlib import Path +from typing import Iterator import matplotlib.pyplot as plt import numpy as np @@ -204,8 +206,8 @@ def Read10X( def ReadOldST( - count_matrix_file: str | Path | None = None, - spatial_file: str | Path | None = None, + count_matrix_file: PathLike[str] | str | Iterator[str], + spatial_file: int | str | bytes | PathLike[str] | PathLike[bytes], image_file: str | Path | None = None, library_id: str = "OldST", scale: float = 1.0, From 74e656c5ace81cdabb0a1bfbbec192eb41a7bda8 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 6 Jun 2025 14:01:55 +1000 Subject: [PATCH 043/241] Remove Literal shim. --- stlearn/_compat.py | 15 --- stlearn/_settings.py | 3 +- stlearn/embedding/umap.py | 4 +- .../image_preprocessing/feature_extractor.py | 3 +- stlearn/preprocessing/graph.py | 4 +- stlearn/preprocessing/normalize.py | 4 +- stlearn/spatials/SME/_weighting_matrix.py | 4 +- stlearn/spatials/SME/impute.py | 2 +- stlearn/spatials/morphology/adjust.py | 4 +- stlearn/tools/clustering/louvain.py | 3 +- stlearn/tools/microenv/cci/het_helpers.py | 120 ------------------ stlearn/wrapper/read.py | 4 +- 12 files changed, 14 insertions(+), 156 deletions(-) delete mode 100644 stlearn/_compat.py diff --git a/stlearn/_compat.py b/stlearn/_compat.py deleted file mode 100644 index ba28b435..00000000 --- a/stlearn/_compat.py +++ /dev/null @@ -1,15 +0,0 @@ -try: - from typing import Literal -except ImportError: - try: - from typing import Literal - except ImportError: - - class LiteralMeta(type): - def __getitem__(cls, values): - if not isinstance(values, tuple): - values = (values,) - return type("Literal_", (Literal,), dict(__args__=values)) - - class Literal(metaclass=LiteralMeta): - pass diff --git a/stlearn/_settings.py b/stlearn/_settings.py index 97276dbd..444612c3 100644 --- a/stlearn/_settings.py +++ b/stlearn/_settings.py @@ -6,10 +6,9 @@ from logging import getLevelName from pathlib import Path from time import time -from typing import Any, TextIO, Iterator +from typing import Any, TextIO, Iterator, Literal from . import logging -from ._compat import Literal from .logging import _RootLogger, _set_log_file, _set_log_level # All the code here migrated from scanpy diff --git a/stlearn/embedding/umap.py b/stlearn/embedding/umap.py index aa509979..ad3079ca 100644 --- a/stlearn/embedding/umap.py +++ b/stlearn/embedding/umap.py @@ -1,10 +1,10 @@ +from typing import Literal + import numpy as np import scanpy from anndata import AnnData from numpy.random.mtrand import RandomState -from .._compat import Literal - _InitPos = Literal["paga", "spectral", "random"] diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index dc07b343..50fe976c 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -1,3 +1,5 @@ +from typing import Literal + import numpy as np import pandas as pd from anndata import AnnData @@ -6,7 +8,6 @@ # Test progress bar from tqdm import tqdm -from .._compat import Literal from .model_zoo import Model, encode _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] diff --git a/stlearn/preprocessing/graph.py b/stlearn/preprocessing/graph.py index 1cfb2d05..ccc381ed 100644 --- a/stlearn/preprocessing/graph.py +++ b/stlearn/preprocessing/graph.py @@ -1,14 +1,12 @@ from collections.abc import Callable, Mapping from types import MappingProxyType -from typing import Any +from typing import Any, Literal import numpy as np import scanpy from anndata import AnnData from numpy.random import RandomState -from .._compat import Literal - _Method = Literal["umap", "gauss", "rapids"] _MetricFn = Callable[[np.ndarray, np.ndarray], float] # from sklearn.metrics.pairwise_distances.__doc__: diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index 35638614..10018d6a 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -1,12 +1,10 @@ from collections.abc import Iterable +from typing import Literal import numpy as np import scanpy from anndata import AnnData -from stlearn._compat import Literal - - def normalize_total( adata: AnnData, target_sum: float | None = None, diff --git a/stlearn/spatials/SME/_weighting_matrix.py b/stlearn/spatials/SME/_weighting_matrix.py index dfc10727..2ff23eb4 100644 --- a/stlearn/spatials/SME/_weighting_matrix.py +++ b/stlearn/spatials/SME/_weighting_matrix.py @@ -1,10 +1,10 @@ +from typing import Literal + import numpy as np from anndata import AnnData from sklearn.metrics import pairwise_distances from tqdm import tqdm -from ..._compat import Literal - _PLATFORM = Literal["Visium", "Old_ST"] _WEIGHTING_MATRIX = Literal[ "weights_matrix_all", diff --git a/stlearn/spatials/SME/impute.py b/stlearn/spatials/SME/impute.py index 2e1b5968..68a20dc3 100644 --- a/stlearn/spatials/SME/impute.py +++ b/stlearn/spatials/SME/impute.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Literal import numpy as np import pandas as pd @@ -8,7 +9,6 @@ import stlearn -from ..._compat import Literal from ._weighting_matrix import ( _PLATFORM, _WEIGHTING_MATRIX, diff --git a/stlearn/spatials/morphology/adjust.py b/stlearn/spatials/morphology/adjust.py index 7aced5f0..8ec70950 100644 --- a/stlearn/spatials/morphology/adjust.py +++ b/stlearn/spatials/morphology/adjust.py @@ -1,10 +1,10 @@ +from typing import Literal + import numpy as np import scipy.spatial as spatial from anndata import AnnData from tqdm import tqdm -from ..._compat import Literal - _SIMILARITY_MATRIX = Literal["cosine", "euclidean", "pearson", "spearman"] diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index 89a74a3c..05972dbb 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -1,12 +1,11 @@ from collections.abc import Mapping, Sequence from types import MappingProxyType -from typing import Any +from typing import Any, Literal from anndata import AnnData from numpy.random.mtrand import RandomState from scipy.sparse import spmatrix import scanpy -from stlearn._compat import Literal from louvain.VertexPartition import MutableVertexPartition def louvain( diff --git a/stlearn/tools/microenv/cci/het_helpers.py b/stlearn/tools/microenv/cci/het_helpers.py index 67386fc2..3c8dc935 100644 --- a/stlearn/tools/microenv/cci/het_helpers.py +++ b/stlearn/tools/microenv/cci/het_helpers.py @@ -226,126 +226,6 @@ def get_data_for_counting(adata, use_label, mix_mode, all_set): ) # neighbourhood_bcs, neighbourhood_indices -def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): - """Retrieves the minimal information necessary to perform edge counting.""" - # First determining how the edge counting needs to be performed # - # Ensuring compatibility with current way of adding label_transfer to object - if use_label == "label_transfer" or use_label == "predictions": - obs_key, uns_key = "predictions", "label_transfer" - else: - obs_key, uns_key = use_label, use_label - - # Getting the neighbourhoods # - neighbours, neighbourhood_bcs, neighbourhood_indices = get_neighbourhoods(adata) - - # Getting the cell type information; if not mixtures then populate - # matrix with one's indicating pure spots. - if mix_mode: - cell_props = adata.uns[uns_key] - cols = cell_props.columns.values.astype(str) - col_order = [ - np.where([cell_type in col for col in cols])[0][0] for cell_type in all_set - ] - cell_data = adata.uns[uns_key].iloc[:, col_order].values.astype(np.float64) - else: - cell_labels = adata.obs.loc[:, obs_key].values - cell_data = np.zeros((len(cell_labels), len(all_set)), dtype=np.float64) - for i, cell_type in enumerate(all_set): - cell_data[:, i] = ( - (cell_labels == cell_type).astype(np.int32).astype(np.float64) - ) - - spot_bcs = adata.obs_names.values.astype(str) - return spot_bcs, cell_data, neighbourhood_bcs, neighbourhood_indices - - -# @njit -def get_neighbourhoods_FAST( - spot_bcs: np.array, - spot_neigh_bcs: np.ndarray, - n_spots: int, - str_dtype: str, - neigh_indices: np.array, - neigh_bcs: np.array, -): - """Gets the neighbourhood information, njit compiled.""" - - # Determining the neighbour spots used for significance testing # - # neighbours = List( numba.int64[:] ) - # neighbourhood_bcs = List((numba.int64, numba.int64[:])) - # neighbourhood_indices = List( (types.unicode_type, types.unicode_type[:]) ) - - # Numba version - # neighbours = List([neigh_indices])[1:] - # neighbourhood_bcs = List() - # neighbourhood_indices = List([(0, neigh_indices)])[1:] - - # Trying normal lists - neighbours, neighbourhood_bcs, neighbourhood_indices = [], [], [] - - for i in range(spot_neigh_bcs.shape[0]): - neigh_bcs = np.array(spot_neigh_bcs[i, :][0].split(",")) - neigh_bcs = neigh_bcs[neigh_bcs != ""] - # neigh_bcs_sub = List() - # for neigh_bc in neigh_bcs: - # if neigh_bc in spot_bcs: - # neigh_bcs_sub.append( neigh_bc ) - - # neigh_bcs_array = np.empty((len(neigh_bcs_sub)), str_dtype) - # neigh_bcs_array = np.empty(len(neigh_bcs_sub), dtype=str_dtype) - # neigh_indices = np.zeros((len(neigh_bcs_sub)), dtype=np.int64) - neigh_bcs_array, neigh_indices = [], [] - for j, neigh_bc in enumerate(neigh_bcs): - - bc_indices = np.where(spot_bcs == neigh_bc)[0] - if len(bc_indices) > 0: - neigh_bcs_array.append(neigh_bc) - neigh_indices.append(bc_indices[0]) - - neigh_bcs_array = np.array(neigh_bcs_array, dtype=str_dtype) - neigh_indices = np.array(neigh_indices, dtype=np.int64) - - neighbours.append(neigh_indices) - neighbourhood_indices.append((i, neigh_indices)) - neighbourhood_bcs.append((spot_bcs[i], neigh_bcs_array)) - - # return neighbours, neighbourhood_bcs, neighbourhood_indices - return List(neighbours), List(neighbourhood_bcs), List(neighbourhood_indices) - - -def get_data_for_counting_OLD(adata, use_label, mix_mode, all_set): - """Retrieves the minimal information necessary to perform edge counting.""" - # First determining how the edge counting needs to be performed # - # Ensuring compatibility with current way of adding label_transfer to object - if use_label == "label_transfer" or use_label == "predictions": - obs_key, uns_key = "predictions", "label_transfer" - else: - obs_key, uns_key = use_label, use_label - - # Getting the neighbourhoods # - neighbours, neighbourhood_bcs, neighbourhood_indices = get_neighbourhoods(adata) - - # Getting the cell type information; if not mixtures then populate - # matrix with one's indicating pure spots. - if mix_mode: - cell_props = adata.uns[uns_key] - cols = cell_props.columns.values.astype(str) - col_order = [ - np.where([cell_type in col for col in cols])[0][0] for cell_type in all_set - ] - cell_data = adata.uns[uns_key].iloc[:, col_order].values.astype(np.float64) - else: - cell_labels = adata.obs.loc[:, obs_key].values - cell_data = np.zeros((len(cell_labels), len(all_set)), dtype=np.float64) - for i, cell_type in enumerate(all_set): - cell_data[:, i] = ( - (cell_labels == cell_type).astype(np.int_).astype(np.float64) - ) - - spot_bcs = adata.obs_names.values.astype(str) - return spot_bcs, cell_data, neighbourhood_bcs, neighbourhood_indices - - def get_neighbourhoods_FAST( spot_bcs: np.array, spot_neigh_bcs: np.ndarray, diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index fbb3fa7a..291dd138 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -4,7 +4,7 @@ import logging as logg from os import PathLike from pathlib import Path -from typing import Iterator +from typing import Iterator, Literal import matplotlib.pyplot as plt import numpy as np @@ -16,8 +16,6 @@ import stlearn -from .._compat import Literal - _Quality = Literal["fulres", "hires", "lowres"] _Background = Literal["black", "white"] From b3e5b780fafcb56c9dfc73b78f5ffa34030c51ef Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 6 Jun 2025 14:12:18 +1000 Subject: [PATCH 044/241] Reformat. --- stlearn/_settings.py | 1 + stlearn/adds/add_loupe_clusters.py | 2 +- stlearn/app/cli.py | 3 +- stlearn/em.py | 2 +- stlearn/logging.py | 196 ++++--- stlearn/pl.py | 1 + stlearn/plotting/cci_plot.py | 3 +- stlearn/plotting/cci_plot_helpers.py | 5 +- stlearn/plotting/classes.py | 482 +++++++++--------- stlearn/plotting/classes_bokeh.py | 1 - stlearn/plotting/cluster_plot.py | 5 +- stlearn/plotting/deconvolution_plot.py | 4 +- stlearn/plotting/feat_plot.py | 3 +- stlearn/plotting/gene_plot.py | 61 +-- stlearn/plotting/subcluster_plot.py | 6 +- .../plotting/trajectory/DE_transition_plot.py | 2 +- .../trajectory/transition_markers_plot.py | 4 +- stlearn/preprocessing/log_scale.py | 8 +- stlearn/preprocessing/normalize.py | 1 + stlearn/spatials/trajectory/local_level.py | 2 +- .../spatials/trajectory/pseudotimespace.py | 2 +- stlearn/tools/clustering/louvain.py | 1 + stlearn/tools/microenv/cci/base_grouping.py | 1 + stlearn/utils.py | 10 +- 24 files changed, 434 insertions(+), 372 deletions(-) diff --git a/stlearn/_settings.py b/stlearn/_settings.py index 444612c3..5e0c28c3 100644 --- a/stlearn/_settings.py +++ b/stlearn/_settings.py @@ -65,6 +65,7 @@ class stLearnConfig: # noqa N801 """\ Config manager for scanpy. """ + _logpath: Path | None _logfile: TextIO _verbosity: Verbosity diff --git a/stlearn/adds/add_loupe_clusters.py b/stlearn/adds/add_loupe_clusters.py index a85b15cf..f257f80f 100644 --- a/stlearn/adds/add_loupe_clusters.py +++ b/stlearn/adds/add_loupe_clusters.py @@ -43,4 +43,4 @@ def add_loupe_clusters( categories=natsorted(label[key_add].unique().astype("U")), ) - return adata if copy else None \ No newline at end of file + return adata if copy else None diff --git a/stlearn/app/cli.py b/stlearn/app/cli.py index 42e92779..4d66f843 100644 --- a/stlearn/app/cli.py +++ b/stlearn/app/cli.py @@ -34,5 +34,6 @@ def launch(): ) from e raise + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/stlearn/em.py b/stlearn/em.py index bfac0db9..5ba9551f 100644 --- a/stlearn/em.py +++ b/stlearn/em.py @@ -13,4 +13,4 @@ "run_ica", "run_fa", "run_diffmap", -] \ No newline at end of file +] diff --git a/stlearn/logging.py b/stlearn/logging.py index cfacdfad..985ee5e8 100644 --- a/stlearn/logging.py +++ b/stlearn/logging.py @@ -28,13 +28,13 @@ def __init__(self, level): _RootLogger.manager = logging.Manager(self) def log_with_timing( - self, - level: int, - msg: str, - *, - extra: dict | None = None, - time: datetime | None = None, - deep: str | None = None, + self, + level: int, + msg: str, + *, + extra: dict | None = None, + time: datetime | None = None, + deep: str | None = None, ) -> datetime: from . import settings @@ -48,14 +48,15 @@ def log_with_timing( super().log(level, msg, extra=extra) return now - def _handle_enhanced_logging(self, level: int, msg, *args, **kwargs) -> Optional[ - datetime]: + def _handle_enhanced_logging( + self, level: int, msg, *args, **kwargs + ) -> Optional[datetime]: """Handle logging with enhanced features (timing, deep info) or fall back to standard logging.""" - if 'time' in kwargs or 'deep' in kwargs or 'extra' in kwargs: + if "time" in kwargs or "deep" in kwargs or "extra" in kwargs: # Extract enhanced arguments - time_arg = kwargs.pop('time', None) - deep_arg = kwargs.pop('deep', None) - extra_arg = kwargs.pop('extra', None) + time_arg = kwargs.pop("time", None) + deep_arg = kwargs.pop("deep", None) + extra_arg = kwargs.pop("extra", None) # Format message if there are remaining args if args or kwargs: @@ -63,83 +64,104 @@ def _handle_enhanced_logging(self, level: int, msg, *args, **kwargs) -> Optional else: formatted_msg = msg - return self.log_with_timing(level, formatted_msg, - time=time_arg, deep=deep_arg, extra=extra_arg) + return self.log_with_timing( + level, formatted_msg, time=time_arg, deep=deep_arg, extra=extra_arg + ) else: super().log(level, msg, *args, **kwargs) return None - def hint(self, msg, *, time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None) -> datetime: + def hint( + self, + msg, + *, + time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, + ) -> datetime: return self.log_with_timing(HINT, msg, time=time, deep=deep, extra=extra) @overload - def debug(self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, - stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None) -> None: - ... + def debug( + self, + msg: object, + *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None, + ) -> None: ... @overload - def debug(self, msg, *args, **kwargs): - ... + def debug(self, msg, *args, **kwargs): ... def debug(self, msg, *args, **kwargs) -> Optional[datetime]: return self._handle_enhanced_logging(DEBUG, msg, *args, **kwargs) @overload - def info(self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, - stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None) -> None: - ... + def info( + self, + msg: object, + *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None, + ) -> None: ... @overload - def info(self, msg, *args, **kwargs): - ... + def info(self, msg, *args, **kwargs): ... def info(self, msg, *args, **kwargs) -> Optional[datetime]: return self._handle_enhanced_logging(INFO, msg, *args, **kwargs) @overload - def warning(self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, - stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None) -> None: - ... + def warning( + self, + msg: object, + *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None, + ) -> None: ... @overload - def warning(self, msg, *args, **kwargs): - ... + def warning(self, msg, *args, **kwargs): ... def warning(self, msg, *args, **kwargs) -> Optional[datetime]: return self._handle_enhanced_logging(WARNING, msg, *args, **kwargs) @overload - def error(self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, - stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None) -> None: - ... + def error( + self, + msg: object, + *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None, + ) -> None: ... @overload - def error(self, msg, *args, **kwargs): - ... + def error(self, msg, *args, **kwargs): ... def error(self, msg, *args, **kwargs) -> Optional[datetime]: return self._handle_enhanced_logging(ERROR, msg, *args, **kwargs) @overload - def critical(self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, - stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None) -> None: - ... + def critical( + self, + msg: object, + *args: object, + exc_info: Union[bool, tuple, BaseException, None] = None, + stack_info: bool = False, + stacklevel: int = 1, + extra: Optional[Mapping[str, object]] = None, + ) -> None: ... @overload - def critical(self, msg, *args, **kwargs): - ... + def critical(self, msg, *args, **kwargs): ... def critical(self, msg, *args, **kwargs) -> Optional[datetime]: return self._handle_enhanced_logging(CRITICAL, msg, *args, **kwargs) @@ -168,7 +190,7 @@ def _set_log_level(settings, level: int): class _LogFormatter(logging.Formatter): def __init__( - self, fmt="{levelname}: {message}", datefmt="%Y-%m-%d %H:%M", style="{" + self, fmt="{levelname}: {message}", datefmt="%Y-%m-%d %H:%M", style="{" ): super().__init__(fmt, datefmt, style) @@ -182,23 +204,19 @@ def format(self, record: logging.LogRecord): self._style._fmt = " {message}" # Handle time_passed if present (should be in extra) - time_passed = getattr(record, 'time_passed', None) + time_passed = getattr(record, "time_passed", None) if time_passed: # Strip microseconds if time_passed.microseconds: - time_passed = timedelta( - seconds=int(time_passed.total_seconds()) - ) + time_passed = timedelta(seconds=int(time_passed.total_seconds())) if "{time_passed}" in record.msg: - record.msg = record.msg.replace( - "{time_passed}", str(time_passed) - ) + record.msg = record.msg.replace("{time_passed}", str(time_passed)) else: self._style._fmt += " ({time_passed})" # Add time_passed to record for formatting record.time_passed = time_passed - deep = getattr(record, 'deep', None) + deep = getattr(record, "deep", None) if deep: record.msg = f"{record.msg}: {deep}" @@ -270,11 +288,11 @@ def _copy_docs_and_signature(fn): def error( - msg: str, - *, - time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None, + msg: str, + *, + time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, ) -> datetime: """\ Log message with specific level and return current time. @@ -301,35 +319,55 @@ def error( @_copy_docs_and_signature(error) -def warning(msg: str, *, time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None) -> datetime: +def warning( + msg: str, + *, + time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, +) -> datetime: from ._settings import settings + result = settings._root_logger.warning(msg, time=time, deep=deep, extra=extra) return result or datetime.now(timezone.utc) @_copy_docs_and_signature(error) -def info(msg: str, *, time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None) -> datetime: +def info( + msg: str, + *, + time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, +) -> datetime: from ._settings import settings + result = settings._root_logger.info(msg, time=time, deep=deep, extra=extra) return result or datetime.now(timezone.utc) @_copy_docs_and_signature(error) -def hint(msg: str, *, time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None) -> datetime: +def hint( + msg: str, + *, + time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, +) -> datetime: from ._settings import settings + return settings._root_logger.hint(msg, time=time, deep=deep, extra=extra) @_copy_docs_and_signature(error) -def debug(msg: str, *, time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None) -> datetime: +def debug( + msg: str, + *, + time: Optional[datetime] = None, + deep: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, +) -> datetime: from ._settings import settings + result = settings._root_logger.debug(msg, time=time, deep=deep, extra=extra) return result or datetime.now(timezone.utc) diff --git a/stlearn/pl.py b/stlearn/pl.py index 56baff5c..9ef4f5d5 100644 --- a/stlearn/pl.py +++ b/stlearn/pl.py @@ -12,6 +12,7 @@ from .plotting.cci_plot import het_plot from .plotting.cci_plot import lr_diagnostics, lr_n_spots, lr_summary, lr_go from .plotting.cci_plot import lr_plot, lr_result_plot + # from .plotting.cci_plot import het_plot_interactive from .plotting.cci_plot import lr_plot_interactive, spatialcci_plot_interactive from .plotting.cluster_plot import cluster_plot diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 5c053747..df983f75 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -3,7 +3,8 @@ import sys from typing import ( Any, - Optional, Tuple, # Special + Optional, + Tuple, # Special ) import matplotlib diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index 9007e83f..de4b243a 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -1,4 +1,5 @@ """Helper functions for cci_plot.py.""" + from typing import List, Tuple, Optional import matplotlib @@ -232,8 +233,8 @@ def add_arrows( int_df: pd.DataFrame | None, head_width: float = 4, width: float = 0.001, - arrow_cmap: str | None =None, - arrow_vmax: float | None =None, + arrow_cmap: str | None = None, + arrow_vmax: float | None = None, ): """ Adds arrows to the current plot for significant spots to neighbours \ which is interacting with. diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 6a6db687..097c3e81 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -7,7 +7,8 @@ import numbers import warnings from typing import ( # Special - Optional, Tuple, # Classes + Optional, + Tuple, # Classes ) import matplotlib @@ -25,31 +26,31 @@ class SpatialBasePlot(Spatial): def __init__( - self, - # plotting param - adata: AnnData, - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - color_bar_label: str = "", - zoom_coord: Tuple[float, float, float, float] | None = None, - crop: bool = True, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 0.7, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - **kwds, + self, + # plotting param + adata: AnnData, + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + zoom_coord: Tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 0.7, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + **kwds, ): super().__init__( adata, @@ -73,7 +74,7 @@ def __init__( if use_label is not None: assert ( - use_label in self.adata[0].obs.columns + use_label in self.adata[0].obs.columns ), "Please choose the right label in `adata.obs.columns`!" self.use_label = use_label @@ -103,8 +104,8 @@ def __init__( stlearn_cmap = ["jana_40", "default"] cmap_available = plt.colormaps() + scanpy_cmap + stlearn_cmap error_msg = ( - "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" - "one of these: " + str(cmap_available) + "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" + "one of these: " + str(cmap_available) ) if cmap is str: assert cmap in cmap_available, error_msg @@ -139,7 +140,7 @@ def create_query(list_cl, use_label): if self.list_clusters is not None: # IF not all clusters specified, subset, otherwise just copy. if len(self.list_clusters) != len( - self.adata[0].obs[self.use_label].cat.categories + self.adata[0].obs[self.use_label].cat.categories ): self.query_adata = self.query_adata[ self.query_adata.obs.query( @@ -181,8 +182,9 @@ def _crop_image(self, main_ax: _AxesSubplot, margin: float): main_ax.set_ylim(self.imagerow.min() - margin, self.imagerow.max() + margin) main_ax.set_ylim(main_ax.get_ylim()[::-1]) - def _zoom_image(self, main_ax: _AxesSubplot, - zoom_coord: Tuple[float, float, float, float]): + def _zoom_image( + self, main_ax: _AxesSubplot, zoom_coord: Tuple[float, float, float, float] + ): main_ax.set_xlim(zoom_coord[0], zoom_coord[1]) main_ax.set_ylim(zoom_coord[3], zoom_coord[2]) @@ -227,39 +229,39 @@ class GenePlot(SpatialBasePlot): gene_symbols: list[str] def __init__( - self, - adata: AnnData, - # plotting param - title: str | None = None, - figsize: Tuple[float, float] | None = None, - cmap: str = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - color_bar_label: str = "", - crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - # gene plot param - gene_symbols: str | list[str] | None = None, - threshold: float | None = None, - method: str = "CumSum", - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: str | None = None, + figsize: Tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # gene plot param + gene_symbols: str | list[str] | None = None, + threshold: float | None = None, + method: str = "CumSum", + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, + **kwargs, ): super().__init__( adata=adata, @@ -440,38 +442,38 @@ def _add_threshold(self, gene_values, threshold): class FeaturePlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - color_bar_label: str = "", - crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - # gene plot param - feature: str | None = None, - threshold: float | None = None, - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # gene plot param + feature: str | None = None, + threshold: float | None = None, + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, + **kwargs, ): super().__init__( adata=adata, @@ -528,7 +530,7 @@ def _get_feature_values(self): self.feature + " is not in data.obs, please try another feature" ) elif not isinstance( - self.query_adata.obs[self.feature].values[0], numbers.Number + self.query_adata.obs[self.feature].values[0], numbers.Number ): raise ValueError( self.feature @@ -604,44 +606,44 @@ def _add_threshold(self, feature_values, threshold): # Cluster plot class class ClusterPlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str = "default", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, - margin: float = 100, - size: float = 5, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - fname: str | None = None, - dpi: int = 120, - # cluster plot param - show_subcluster: bool = False, - show_cluster_labels: bool = False, - show_trajectories: bool = False, - reverse: bool = False, - show_node: bool = False, - threshold_spots: int = 5, - text_box_size: float = 5, - color_bar_size: float = 10, - bbox_to_anchor: tuple[float, float] | None = (1, 1), - # trajectory - trajectory_node_size: int = 10, - trajectory_alpha: float = 1.0, - trajectory_width: float = 2.5, - trajectory_edge_color: str = "#f4efd3", - trajectory_arrowsize: int = 17, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "default", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 5, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + fname: str | None = None, + dpi: int = 120, + # cluster plot param + show_subcluster: bool = False, + show_cluster_labels: bool = False, + show_trajectories: bool = False, + reverse: bool = False, + show_node: bool = False, + threshold_spots: int = 5, + text_box_size: float = 5, + color_bar_size: float = 10, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + # trajectory + trajectory_node_size: int = 10, + trajectory_alpha: float = 1.0, + trajectory_width: float = 2.5, + trajectory_edge_color: str = "#f4efd3", + trajectory_arrowsize: int = 17, ): super().__init__( adata=adata, @@ -766,7 +768,7 @@ def _add_cluster_labels(self): label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ].index + ].index ) subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), label_index) @@ -809,7 +811,7 @@ def _add_sub_clusters(self): label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ].index + ].index ) subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), label_index) @@ -819,18 +821,18 @@ def _add_sub_clusters(self): imgrow_new = subset_spatial[:, 1] * self.scale_factor if ( - len( - self.query_adata.obs[ - self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"].unique() - ) - < 2 + len( + self.query_adata.obs[ + self.query_adata.obs[self.use_label] == str(label) + ]["sub_cluster_labels"].unique() + ) + < 2 ): centroids = [centroidpython(imgcol_new, imgrow_new)] classes = np.array( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"].unique() + ]["sub_cluster_labels"].unique() ) else: @@ -841,7 +843,7 @@ def _add_sub_clusters(self): np.column_stack((imgcol_new, imgrow_new)), self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) - ]["sub_cluster_labels"], + ]["sub_cluster_labels"], ) centroids = clf.centroids_ @@ -849,12 +851,12 @@ def _add_sub_clusters(self): for j, label in enumerate(classes): if ( - len( - self.query_adata.obs[ - self.query_adata.obs["sub_cluster_labels"] == label - ] - ) - > self.threshold_spots + len( + self.query_adata.obs[ + self.query_adata.obs["sub_cluster_labels"] == label + ] + ) + > self.threshold_spots ): if centroids[j][0] < 1500: x = -100 @@ -956,34 +958,34 @@ def _add_trajectories(self): class SubClusterPlot(SpatialBasePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str = "jet", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, - margin: float = 100, - size: float = 5, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - fname: str | None = None, - dpi: int = 120, - # subcluster plot param - cluster: int = 0, - threshold_spots: int = 5, - text_box_size: float = 5, - bbox_to_anchor: tuple[float, float] | None = (1, 1), - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "jet", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 5, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + fname: str | None = None, + dpi: int = 120, + # subcluster plot param + cluster: int = 0, + threshold_spots: int = 5, + text_box_size: float = 5, + bbox_to_anchor: tuple[float, float] | None = (1, 1), + **kwargs, ): super().__init__( adata=adata, @@ -1110,36 +1112,36 @@ def _add_subclusters_label(self, subset): class CciPlot(GenePlot): def __init__( - self, - adata: AnnData, - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - # cci_rank param - use_het: str = "het", - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, - **kwargs, + self, + adata: AnnData, + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # cci_rank param + use_het: str = "het", + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, + **kwargs, ): super().__init__( adata=adata, @@ -1179,36 +1181,36 @@ def _get_gene_expression(self): class LrResultPlot(GenePlot): def __init__( - self, - adata: AnnData, - use_lr: Optional["str"] = None, - use_result: Optional["str"] = "lr_sig_scores", - # plotting param - title: Optional["str"] = None, - figsize: tuple[float, float] | None = None, - cmap: str = "Spectral_r", - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - # cci_rank param - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, - **kwargs, + self, + adata: AnnData, + use_lr: Optional["str"] = None, + use_result: Optional["str"] = "lr_sig_scores", + # plotting param + title: Optional["str"] = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + crop: bool = True, + zoom_coord: Tuple[float, float, float, float] | None = None, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # cci_rank param + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, + **kwargs, ): # Making sure cci_rank has been run first # if "lr_summary" not in adata.uns: diff --git a/stlearn/plotting/classes_bokeh.py b/stlearn/plotting/classes_bokeh.py index 2aff9167..9b9e8ee9 100644 --- a/stlearn/plotting/classes_bokeh.py +++ b/stlearn/plotting/classes_bokeh.py @@ -782,7 +782,6 @@ def __init__( else: image = (self.img * 255).astype(np.uint8) - img_pillow = Image.fromarray(image).convert("RGBA") self.xdim, self.ydim = img_pillow.size diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index 8c3194e0..e2f4e47e 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -1,5 +1,6 @@ from typing import ( - Optional, Tuple, # Special + Optional, + Tuple, # Special ) import matplotlib @@ -44,7 +45,7 @@ def cluster_plot( show_node: bool = False, threshold_spots: int = 5, text_box_size: float = 5, - color_bar_size: float= 10, + color_bar_size: float = 10, bbox_to_anchor: tuple[float, float] | None = (1, 1), # trajectory trajectory_node_size: int = 10, diff --git a/stlearn/plotting/deconvolution_plot.py b/stlearn/plotting/deconvolution_plot.py index 571bd370..f0c92a5c 100644 --- a/stlearn/plotting/deconvolution_plot.py +++ b/stlearn/plotting/deconvolution_plot.py @@ -15,7 +15,9 @@ def deconvolution_plot( data_alpha: float = 1.0, threshold: float = 0.0, cmap: str = "tab20", - colors: list[tuple[float, float, float, float]] | None = None, # The colors to use for each label... + colors: ( + list[tuple[float, float, float, float]] | None + ) = None, # The colors to use for each label... spot_size: float | int = 10, show_axis: bool = False, show_legend: bool = True, diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index e4256bc9..092b7ff4 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -3,7 +3,8 @@ """ from typing import ( - Optional, Tuple, # Special + Optional, + Tuple, # Special ) import matplotlib diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index 7b5c3285..03489251 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -1,5 +1,6 @@ from typing import ( # Special - Optional, Tuple, # Classes + Optional, + Tuple, # Classes ) import matplotlib @@ -15,35 +16,35 @@ @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) def gene_plot( - adata: AnnData, - gene_symbols: str | list | None = None, - threshold: float | None = None, - method: str = "CumSum", - contour: bool = False, - step_size: int | None = None, - title: str | None = None, - figsize: Tuple[float, float] | None = None, - cmap: str = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: matplotlib.axes.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - color_bar_label: str = "", - zoom_coord: Tuple[float, float, float, float] | None = None, - crop: bool = True, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 0.7, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - vmin: float | None = None, - vmax: float | None = None, + adata: AnnData, + gene_symbols: str | list | None = None, + threshold: float | None = None, + method: str = "CumSum", + contour: bool = False, + step_size: int | None = None, + title: str | None = None, + figsize: Tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: matplotlib.axes.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + color_bar_label: str = "", + zoom_coord: Tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 0.7, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + vmin: float | None = None, + vmax: float | None = None, ) -> AnnData | None: """\ Allows the visualization of a single gene or multiple genes as the values diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/plotting/subcluster_plot.py index c6f9c77c..0e8c66e5 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/plotting/subcluster_plot.py @@ -31,11 +31,11 @@ def subcluster_plot( image_alpha: float = 1.0, cell_alpha: float = 1.0, fname: str | None = None, - dpi: int = 120, + dpi: int = 120, # subcluster plot param cluster: int = 0, threshold_spots: int = 5, - text_box_size: float= 5, + text_box_size: float = 5, bbox_to_anchor: tuple[float, float] | None = (1, 1), ) -> AnnData | None: """\ @@ -87,4 +87,4 @@ def subcluster_plot( threshold_spots=threshold_spots, ) - return adata \ No newline at end of file + return adata diff --git a/stlearn/plotting/trajectory/DE_transition_plot.py b/stlearn/plotting/trajectory/DE_transition_plot.py index 5f7b5147..1ea91831 100644 --- a/stlearn/plotting/trajectory/DE_transition_plot.py +++ b/stlearn/plotting/trajectory/DE_transition_plot.py @@ -240,4 +240,4 @@ def DE_transition_plot( if name is not None: plt.savefig(output + "/" + name, dpi=dpi, bbox_inches="tight", pad_inches=0) - return adata \ No newline at end of file + return adata diff --git a/stlearn/plotting/trajectory/transition_markers_plot.py b/stlearn/plotting/trajectory/transition_markers_plot.py index cf93e93d..c816b193 100644 --- a/stlearn/plotting/trajectory/transition_markers_plot.py +++ b/stlearn/plotting/trajectory/transition_markers_plot.py @@ -35,7 +35,9 @@ def transition_markers_plot( """ if trajectory not in adata.uns: - raise ValueError("Please input the right trajectory name - not found in adata.uns!") + raise ValueError( + "Please input the right trajectory name - not found in adata.uns!" + ) pos = ( adata.uns[trajectory][adata.uns[trajectory]["score"] >= 0] diff --git a/stlearn/preprocessing/log_scale.py b/stlearn/preprocessing/log_scale.py index 0eb1cd1b..2faf99cf 100644 --- a/stlearn/preprocessing/log_scale.py +++ b/stlearn/preprocessing/log_scale.py @@ -38,7 +38,9 @@ def log1p( Returns or updates `data`, depending on `copy`. """ - result = scanpy.pp.log1p(adata, copy=copy, chunked=chunked, chunk_size=chunk_size, base=base) + result = scanpy.pp.log1p( + adata, copy=copy, chunked=chunked, chunk_size=chunk_size, base=base + ) print("Log transformation step is finished in adata.X") return result @@ -75,6 +77,8 @@ def scale( Depending on `copy` returns or updates `adata` with a scaled `adata.X`. """ - result = scanpy.pp.scale(adata, zero_center=zero_center, max_value=max_value, copy=copy) + result = scanpy.pp.scale( + adata, zero_center=zero_center, max_value=max_value, copy=copy + ) print("Scale step is finished in adata.X") return result diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index 10018d6a..376a2f04 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -5,6 +5,7 @@ import scanpy from anndata import AnnData + def normalize_total( adata: AnnData, target_sum: float | None = None, diff --git a/stlearn/spatials/trajectory/local_level.py b/stlearn/spatials/trajectory/local_level.py index 71a155a3..3be6dd0b 100644 --- a/stlearn/spatials/trajectory/local_level.py +++ b/stlearn/spatials/trajectory/local_level.py @@ -81,4 +81,4 @@ def local_level( if return_matrix: return stdm else: - return None \ No newline at end of file + return None diff --git a/stlearn/spatials/trajectory/pseudotimespace.py b/stlearn/spatials/trajectory/pseudotimespace.py index 1dec4db9..acd52725 100644 --- a/stlearn/spatials/trajectory/pseudotimespace.py +++ b/stlearn/spatials/trajectory/pseudotimespace.py @@ -102,4 +102,4 @@ def pseudotimespace_local( local_level(adata, use_label=use_label, cluster=cluster, w=w) - return adata \ No newline at end of file + return adata diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index 05972dbb..27bcbf5d 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -8,6 +8,7 @@ import scanpy from louvain.VertexPartition import MutableVertexPartition + def louvain( adata: AnnData, resolution: float | None = None, diff --git a/stlearn/tools/microenv/cci/base_grouping.py b/stlearn/tools/microenv/cci/base_grouping.py index a980ae9d..ac8fee7a 100644 --- a/stlearn/tools/microenv/cci/base_grouping.py +++ b/stlearn/tools/microenv/cci/base_grouping.py @@ -11,6 +11,7 @@ from tqdm import tqdm from stlearn.pl import het_plot + def get_hotspots( adata: AnnData, lr_scores: np.ndarray, diff --git a/stlearn/utils.py b/stlearn/utils.py index a4531ea0..a9b5cb38 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -37,7 +37,6 @@ def _check_spot_size(spatial_data: Mapping | None, spot_size: float | None) -> f return spatial_data["scalefactors"]["spot_diameter_fullres"] - def _check_scale_factor( spatial_data: Mapping | None, img_key: str | None, @@ -118,7 +117,12 @@ def _check_img( if img_key is _empty: # Looks for image - or None if not found. new_img_key = next( - (k for k in ["hires", "lowres", "fulres"] if k in spatial_data["images"]), None + ( + k + for k in ["hires", "lowres", "fulres"] + if k in spatial_data["images"] + ), + None, ) else: new_img_key = img_key @@ -133,7 +137,7 @@ def _check_img( def _check_coords( - obsm: Mapping | None, scale_factor: float | None + obsm: Mapping | None, scale_factor: float | None ) -> tuple[np.ndarray, np.ndarray]: if obsm is None: raise ValueError("obsm cannot be None") From f2ed7fd76ed8a521764fbe839cbd76d936a603e2 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 6 Jun 2025 14:56:32 +1000 Subject: [PATCH 045/241] Fix style checks. --- stlearn/_settings.py | 8 +- stlearn/adds/add_mask.py | 1 + stlearn/adds/parsing.py | 1 - stlearn/app/app.py | 14 +--- stlearn/app/cli.py | 1 + stlearn/app/source/forms/views.py | 4 +- stlearn/em.py | 8 +- stlearn/logging.py | 83 ++++++++++--------- stlearn/pl.py | 31 +++---- stlearn/plotting/cci_plot.py | 7 +- stlearn/plotting/cci_plot_helpers.py | 7 +- stlearn/plotting/classes.py | 23 +++-- stlearn/plotting/cluster_plot.py | 5 +- stlearn/plotting/feat_plot.py | 5 +- stlearn/plotting/gene_plot.py | 8 +- stlearn/plotting/trajectory/tree_plot.py | 3 +- .../plotting/trajectory/tree_plot_simple.py | 3 +- stlearn/spatials/trajectory/local_level.py | 3 +- stlearn/spatials/trajectory/utils.py | 2 - stlearn/tools/clustering/louvain.py | 4 +- stlearn/tools/microenv/cci/base_grouping.py | 1 + stlearn/tools/microenv/cci/het.py | 4 +- stlearn/utils.py | 6 +- stlearn/wrapper/read.py | 3 +- 24 files changed, 113 insertions(+), 122 deletions(-) diff --git a/stlearn/_settings.py b/stlearn/_settings.py index 5e0c28c3..9e75a8d4 100644 --- a/stlearn/_settings.py +++ b/stlearn/_settings.py @@ -1,12 +1,12 @@ import inspect import sys -from collections.abc import Iterable -from contextlib import AbstractContextManager, contextmanager +from collections.abc import Iterable, Iterator +from contextlib import contextmanager from enum import IntEnum from logging import getLevelName from pathlib import Path from time import time -from typing import Any, TextIO, Iterator, Literal +from typing import Any, Literal, TextIO from . import logging from .logging import _RootLogger, _set_log_file, _set_log_level @@ -363,7 +363,7 @@ def logfile(self, logfile: str | Path | TextIO | None): if logfile is None or logfile == "": self._logfile = sys.stdout if self._is_run_from_ipython() else sys.stderr self._logpath = None - elif isinstance(logfile, (str, Path)): + elif isinstance(logfile, (str | Path)): path = Path(logfile) self._logfile = path.open("a") self._logpath = path diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index 680885f8..998b4936 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -105,6 +105,7 @@ def apply_mask( Array format of image, saving by Pillow package. """ from scanpy.plotting import palettes + from stlearn.plotting import palettes_st adata = adata.copy() if copy else adata diff --git a/stlearn/adds/parsing.py b/stlearn/adds/parsing.py index 4b824c59..0ae6a9f0 100644 --- a/stlearn/adds/parsing.py +++ b/stlearn/adds/parsing.py @@ -1,5 +1,4 @@ from os import PathLike -from pathlib import Path import numpy as np from anndata import AnnData diff --git a/stlearn/app/app.py b/stlearn/app/app.py index 7343ceeb..1e393468 100644 --- a/stlearn/app/app.py +++ b/stlearn/app/app.py @@ -1,17 +1,9 @@ import os -import subprocess import sys from threading import Thread sys.path.append(os.path.dirname(__file__)) -try: - import flask -except ImportError: - subprocess.call( - "pip install -r " + os.path.dirname(__file__) + "//requirements.txt", shell=True - ) - import asyncio import tempfile @@ -32,14 +24,14 @@ send_file, url_for, ) - -# Functions related to processing the forms. -from stlearn.app.source.forms import views # for changing data in response to input from tornado.ioloop import IOLoop from werkzeug.utils import secure_filename import stlearn +# Functions related to processing the forms. +from stlearn.app.source.forms import views # for changing data in response to input + # Global variables. global adata # Storing the data diff --git a/stlearn/app/cli.py b/stlearn/app/cli.py index 4d66f843..78bfe02b 100644 --- a/stlearn/app/cli.py +++ b/stlearn/app/cli.py @@ -1,4 +1,5 @@ import errno + import click from .. import __version__ diff --git a/stlearn/app/source/forms/views.py b/stlearn/app/source/forms/views.py index 30a2c672..3dbeed18 100644 --- a/stlearn/app/source/forms/views.py +++ b/stlearn/app/source/forms/views.py @@ -10,12 +10,12 @@ import numpy as np import scanpy as sc from flask import flash, render_template + +import stlearn as st import stlearn.app.source.forms.view_helpers as vhs from stlearn.app.source.forms import forms from stlearn.app.source.forms.utils import flash_errors -import stlearn as st - # Creating the forms using a class generator # PreprocessForm = forms.getPreprocessForm() # CCIForm = forms.getCCIForm() #OLD diff --git a/stlearn/em.py b/stlearn/em.py index 5ba9551f..39d0c2db 100644 --- a/stlearn/em.py +++ b/stlearn/em.py @@ -1,11 +1,11 @@ # from .embedding.scvi import run_ldvae -from .embedding.pca import run_pca -from .embedding.umap import run_umap -from .embedding.ica import run_ica +from .embedding.diffmap import run_diffmap # from .embedding.scvi import run_ldvae from .embedding.fa import run_fa -from .embedding.diffmap import run_diffmap +from .embedding.ica import run_ica +from .embedding.pca import run_pca +from .embedding.umap import run_umap __all__ = [ "run_pca", diff --git a/stlearn/logging.py b/stlearn/logging.py index 985ee5e8..be37ad1f 100644 --- a/stlearn/logging.py +++ b/stlearn/logging.py @@ -1,10 +1,11 @@ """Logging and Profiling""" import logging +from collections.abc import Mapping from datetime import datetime, timedelta, timezone from functools import partial, update_wrapper from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING -from typing import Dict, Any, Optional, overload, Mapping, Union +from typing import Any, overload import anndata.logging @@ -13,12 +14,13 @@ class CustomLogRecord(logging.LogRecord): - """Custom root logger that maintains compatibility with standard logging interface.""" + """Custom root logger that maintains compatibility with standard logging + interface.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.time_passed: Optional[timedelta] = None - self.deep: Optional[str] = None + self.time_passed: timedelta | None = None + self.deep: str | None = None class _RootLogger(logging.RootLogger): @@ -39,7 +41,7 @@ def log_with_timing( from . import settings now = datetime.now(timezone.utc) - time_passed: Optional[timedelta] = None if time is None else now - time + time_passed: timedelta | None = None if time is None else now - time extra = { **(extra or {}), "deep": deep if settings.verbosity.level < level else None, @@ -50,8 +52,9 @@ def log_with_timing( def _handle_enhanced_logging( self, level: int, msg, *args, **kwargs - ) -> Optional[datetime]: - """Handle logging with enhanced features (timing, deep info) or fall back to standard logging.""" + ) -> datetime | None: + """Handle logging with enhanced features (timing, deep info) or fall back to + standard logging.""" if "time" in kwargs or "deep" in kwargs or "extra" in kwargs: # Extract enhanced arguments time_arg = kwargs.pop("time", None) @@ -75,9 +78,9 @@ def hint( self, msg, *, - time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None, + time: datetime | None = None, + deep: str | None = None, + extra: dict[str, Any] | None = None, ) -> datetime: return self.log_with_timing(HINT, msg, time=time, deep=deep, extra=extra) @@ -86,16 +89,16 @@ def debug( self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, + exc_info: bool | tuple | BaseException | None = None, stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None, + extra: Mapping[str, object] | None = None, ) -> None: ... @overload def debug(self, msg, *args, **kwargs): ... - def debug(self, msg, *args, **kwargs) -> Optional[datetime]: + def debug(self, msg, *args, **kwargs) -> datetime | None: return self._handle_enhanced_logging(DEBUG, msg, *args, **kwargs) @overload @@ -103,16 +106,16 @@ def info( self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, + exc_info: bool | tuple | BaseException | None = None, stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None, + extra: Mapping[str, object] | None = None, ) -> None: ... @overload def info(self, msg, *args, **kwargs): ... - def info(self, msg, *args, **kwargs) -> Optional[datetime]: + def info(self, msg, *args, **kwargs) -> datetime | None: return self._handle_enhanced_logging(INFO, msg, *args, **kwargs) @overload @@ -120,16 +123,16 @@ def warning( self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, + exc_info: bool | tuple | BaseException | None = None, stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None, + extra: Mapping[str, object] | None = None, ) -> None: ... @overload def warning(self, msg, *args, **kwargs): ... - def warning(self, msg, *args, **kwargs) -> Optional[datetime]: + def warning(self, msg, *args, **kwargs) -> datetime | None: return self._handle_enhanced_logging(WARNING, msg, *args, **kwargs) @overload @@ -137,16 +140,16 @@ def error( self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, + exc_info: bool | tuple | BaseException | None = None, stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None, + extra: Mapping[str, object] | None = None, ) -> None: ... @overload def error(self, msg, *args, **kwargs): ... - def error(self, msg, *args, **kwargs) -> Optional[datetime]: + def error(self, msg, *args, **kwargs) -> datetime | None: return self._handle_enhanced_logging(ERROR, msg, *args, **kwargs) @overload @@ -154,16 +157,16 @@ def critical( self, msg: object, *args: object, - exc_info: Union[bool, tuple, BaseException, None] = None, + exc_info: bool | tuple | BaseException | None = None, stack_info: bool = False, stacklevel: int = 1, - extra: Optional[Mapping[str, object]] = None, + extra: Mapping[str, object] | None = None, ) -> None: ... @overload def critical(self, msg, *args, **kwargs): ... - def critical(self, msg, *args, **kwargs) -> Optional[datetime]: + def critical(self, msg, *args, **kwargs) -> datetime | None: return self._handle_enhanced_logging(CRITICAL, msg, *args, **kwargs) @@ -290,9 +293,9 @@ def _copy_docs_and_signature(fn): def error( msg: str, *, - time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None, + time: datetime | None = None, + deep: str | None = None, + extra: dict[str, Any] | None = None, ) -> datetime: """\ Log message with specific level and return current time. @@ -322,9 +325,9 @@ def error( def warning( msg: str, *, - time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None, + time: datetime | None = None, + deep: str | None = None, + extra: dict[str, Any] | None = None, ) -> datetime: from ._settings import settings @@ -336,9 +339,9 @@ def warning( def info( msg: str, *, - time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None, + time: datetime | None = None, + deep: str | None = None, + extra: dict[str, Any] | None = None, ) -> datetime: from ._settings import settings @@ -350,9 +353,9 @@ def info( def hint( msg: str, *, - time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None, + time: datetime | None = None, + deep: str | None = None, + extra: dict[str, Any] | None = None, ) -> datetime: from ._settings import settings @@ -363,9 +366,9 @@ def hint( def debug( msg: str, *, - time: Optional[datetime] = None, - deep: Optional[str] = None, - extra: Optional[Dict[str, Any]] = None, + time: datetime | None = None, + deep: str | None = None, + extra: dict[str, Any] | None = None, ) -> datetime: from ._settings import settings diff --git a/stlearn/pl.py b/stlearn/pl.py index 9ef4f5d5..78db0c8a 100644 --- a/stlearn/pl.py +++ b/stlearn/pl.py @@ -1,28 +1,31 @@ # from .plotting.cci_plot import het_plot_interactive from .plotting import trajectory -from .plotting.QC_plot import QC_plot + +# from .plotting.cci_plot import het_plot_interactive from .plotting.cci_plot import ( - ccinet_plot, + cci_check, cci_map, + ccinet_plot, + grid_plot, + het_plot, lr_cci_map, lr_chord_plot, - cci_check, + lr_diagnostics, + lr_go, + lr_n_spots, + lr_plot, + lr_plot_interactive, + lr_result_plot, + lr_summary, + spatialcci_plot_interactive, ) -from .plotting.cci_plot import grid_plot -from .plotting.cci_plot import het_plot -from .plotting.cci_plot import lr_diagnostics, lr_n_spots, lr_summary, lr_go -from .plotting.cci_plot import lr_plot, lr_result_plot - -# from .plotting.cci_plot import het_plot_interactive -from .plotting.cci_plot import lr_plot_interactive, spatialcci_plot_interactive -from .plotting.cluster_plot import cluster_plot -from .plotting.cluster_plot import cluster_plot_interactive +from .plotting.cluster_plot import cluster_plot, cluster_plot_interactive from .plotting.deconvolution_plot import deconvolution_plot from .plotting.feat_plot import feat_plot -from .plotting.gene_plot import gene_plot -from .plotting.gene_plot import gene_plot_interactive +from .plotting.gene_plot import gene_plot, gene_plot_interactive from .plotting.mask_plot import plot_mask from .plotting.non_spatial_plot import non_spatial_plot +from .plotting.QC_plot import QC_plot from .plotting.stack_3d_plot import stack_3d_plot from .plotting.subcluster_plot import subcluster_plot diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index df983f75..0f8b0d5d 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -3,8 +3,7 @@ import sys from typing import ( Any, - Optional, - Tuple, # Special + Optional, # Special ) import matplotlib @@ -434,7 +433,7 @@ def lr_result_plot( show_axis: bool = False, show_image: bool = True, show_color_bar: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, crop: bool = True, margin: float = 100, size: float = 7, @@ -904,7 +903,7 @@ def het_plot( show_axis: bool = False, show_image: bool = True, show_color_bar: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, crop: bool = True, margin: float = 100, size: float = 7, diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index de4b243a..b94f147f 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -1,6 +1,5 @@ """Helper functions for cci_plot.py.""" -from typing import List, Tuple, Optional import matplotlib import matplotlib.cm as cm @@ -275,7 +274,7 @@ def add_arrows( interact_bool = int_df.values > 0 # Subsetting to only significant CCI # - edges_sub: List[List[Tuple[str, str]]] = [[], []] # forward, reverse + edges_sub: list[list[tuple[str, str]]] = [[], []] # forward, reverse # ints_2 = np.zeros(int_df.shape) # Just for debugging make sure edge # list re-capitulates edge-counts. for i, edges in enumerate([forward_edges, reverse_edges]): @@ -301,7 +300,7 @@ def add_arrows( # If cmap specified, colour arrows by average LR expression on edge # if arrow_cmap is not None: - edges_means: List[List[float]] = [[], []] + edges_means: list[list[float]] = [[], []] all_means = [] for i, edges in enumerate([forward_edges, reverse_edges]): for j, edge in enumerate(edges): @@ -322,7 +321,7 @@ def add_arrows( scalar_map = cm.ScalarMappable(norm=c_norm, cmap=cmap) # Determining the edge colors # - edges_colors: List[List[Tuple[float, float, float, float]]] = [[], []] + edges_colors: list[list[tuple[float, float, float, float]]] = [[], []] for i, edges in enumerate([forward_edges, reverse_edges]): for j, edge in enumerate(edges): color_val = scalar_map.to_rgba(edges_means[i][j]) diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 097c3e81..d40fcb2d 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -7,8 +7,7 @@ import numbers import warnings from typing import ( # Special - Optional, - Tuple, # Classes + Optional, # Classes ) import matplotlib @@ -19,9 +18,9 @@ from anndata import AnnData from scipy.interpolate import griddata -from .utils import centroidpython, check_sublist, get_cluster, get_cmap, get_node from ..classes import Spatial from ..utils import Axes, _AxesSubplot, _read_graph +from .utils import centroidpython, check_sublist, get_cluster, get_cmap, get_node class SpatialBasePlot(Spatial): @@ -41,7 +40,7 @@ def __init__( show_image: bool = True, show_color_bar: bool = True, color_bar_label: str = "", - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, crop: bool = True, margin: float = 100, size: float = 7, @@ -183,7 +182,7 @@ def _crop_image(self, main_ax: _AxesSubplot, margin: float): main_ax.set_ylim(main_ax.get_ylim()[::-1]) def _zoom_image( - self, main_ax: _AxesSubplot, zoom_coord: Tuple[float, float, float, float] + self, main_ax: _AxesSubplot, zoom_coord: tuple[float, float, float, float] ): main_ax.set_xlim(zoom_coord[0], zoom_coord[1]) @@ -233,7 +232,7 @@ def __init__( adata: AnnData, # plotting param title: str | None = None, - figsize: Tuple[float, float] | None = None, + figsize: tuple[float, float] | None = None, cmap: str = "Spectral_r", use_label: str | None = None, list_clusters: list | None = None, @@ -245,7 +244,7 @@ def __init__( show_color_bar: bool = True, color_bar_label: str = "", crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, margin: float = 100, size: float = 7, image_alpha: float = 1.0, @@ -458,7 +457,7 @@ def __init__( show_color_bar: bool = True, color_bar_label: str = "", crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, margin: float = 100, size: float = 7, image_alpha: float = 1.0, @@ -621,7 +620,7 @@ def __init__( show_image: bool = True, show_color_bar: bool = True, crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, margin: float = 100, size: float = 5, image_alpha: float = 1.0, @@ -973,7 +972,7 @@ def __init__( show_image: bool = True, show_color_bar: bool = True, crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, margin: float = 100, size: float = 5, image_alpha: float = 1.0, @@ -1127,7 +1126,7 @@ def __init__( show_image: bool = True, show_color_bar: bool = True, crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, margin: float = 100, size: float = 7, image_alpha: float = 1.0, @@ -1197,7 +1196,7 @@ def __init__( show_image: bool = True, show_color_bar: bool = True, crop: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, margin: float = 100, size: float = 7, image_alpha: float = 1.0, diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index e2f4e47e..2eb6a66c 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -1,6 +1,5 @@ from typing import ( - Optional, - Tuple, # Special + Optional, # Special ) import matplotlib @@ -29,7 +28,7 @@ def cluster_plot( show_axis: bool = False, show_image: bool = True, show_color_bar: bool = True, - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, crop: bool = True, margin: float = 100, size: float = 5, diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index 092b7ff4..e47450e9 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -3,8 +3,7 @@ """ from typing import ( - Optional, - Tuple, # Special + Optional, # Special ) import matplotlib @@ -32,7 +31,7 @@ def feat_plot( show_image: bool = True, show_color_bar: bool = True, color_bar_label: str = "", - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, crop: bool = True, margin: float = 100, size: float = 7, diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index 03489251..f7f770e0 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -1,7 +1,3 @@ -from typing import ( # Special - Optional, - Tuple, # Classes -) import matplotlib from anndata import AnnData @@ -23,7 +19,7 @@ def gene_plot( contour: bool = False, step_size: int | None = None, title: str | None = None, - figsize: Tuple[float, float] | None = None, + figsize: tuple[float, float] | None = None, cmap: str = "Spectral_r", use_label: str | None = None, list_clusters: list | None = None, @@ -34,7 +30,7 @@ def gene_plot( show_image: bool = True, show_color_bar: bool = True, color_bar_label: str = "", - zoom_coord: Tuple[float, float, float, float] | None = None, + zoom_coord: tuple[float, float, float, float] | None = None, crop: bool = True, margin: float = 100, size: float = 7, diff --git a/stlearn/plotting/trajectory/tree_plot.py b/stlearn/plotting/trajectory/tree_plot.py index 88583991..ce753128 100644 --- a/stlearn/plotting/trajectory/tree_plot.py +++ b/stlearn/plotting/trajectory/tree_plot.py @@ -1,6 +1,5 @@ import math import random -from typing import Tuple import networkx as nx from anndata import AnnData @@ -12,7 +11,7 @@ def tree_plot( adata: AnnData, library_id: str | None = None, - figsize: Tuple[float, float] = (10, 4), + figsize: tuple[float, float] = (10, 4), data_alpha: float = 1.0, use_label: str = "louvain", spot_size: float | int = 50, diff --git a/stlearn/plotting/trajectory/tree_plot_simple.py b/stlearn/plotting/trajectory/tree_plot_simple.py index 03f1b7bd..0dd9902c 100644 --- a/stlearn/plotting/trajectory/tree_plot_simple.py +++ b/stlearn/plotting/trajectory/tree_plot_simple.py @@ -1,6 +1,5 @@ import math import random -from typing import Tuple import networkx as nx from anndata import AnnData @@ -12,7 +11,7 @@ def tree_plot_simple( adata: AnnData, library_id: str | None = None, - figsize: Tuple[float, float] = (10, 4), + figsize: tuple[float, float] = (10, 4), data_alpha: float = 1.0, use_label: str = "louvain", spot_size: float | int = 50, diff --git a/stlearn/spatials/trajectory/local_level.py b/stlearn/spatials/trajectory/local_level.py index 3be6dd0b..a0490b88 100644 --- a/stlearn/spatials/trajectory/local_level.py +++ b/stlearn/spatials/trajectory/local_level.py @@ -30,7 +30,8 @@ def local_level( Return PTS matrix for local level Returns ------- - np.ndarray: the STDM (spatio-temporal distance matrix) - weighted combination of spatial and temporal distances. + np.ndarray: the STDM (spatio-temporal distance matrix) - weighted combination of + spatial and temporal distances. adata["nonabs_dpt_distance_matrix"]: np.ndarray Pseudotime distance (difference between values) matrix diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatials/trajectory/utils.py index f4328f50..46d700fb 100644 --- a/stlearn/spatials/trajectory/utils.py +++ b/stlearn/spatials/trajectory/utils.py @@ -1,4 +1,3 @@ -from collections.abc import Sequence import networkx as nx import numpy as np @@ -575,7 +574,6 @@ def _correlation_test_helper( ) n = X.shape[1] # genes x cells - ql = 1 - confidence_level - (1 - confidence_level) / 2.0 qh = confidence_level + (1 - confidence_level) / 2.0 if issparse(X) and not isspmatrix_csr(X): diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index 27bcbf5d..e0426662 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -2,11 +2,11 @@ from types import MappingProxyType from typing import Any, Literal +import scanpy from anndata import AnnData +from louvain.VertexPartition import MutableVertexPartition from numpy.random.mtrand import RandomState from scipy.sparse import spmatrix -import scanpy -from louvain.VertexPartition import MutableVertexPartition def louvain( diff --git a/stlearn/tools/microenv/cci/base_grouping.py b/stlearn/tools/microenv/cci/base_grouping.py index ac8fee7a..24201a71 100644 --- a/stlearn/tools/microenv/cci/base_grouping.py +++ b/stlearn/tools/microenv/cci/base_grouping.py @@ -9,6 +9,7 @@ from anndata import AnnData from sklearn.cluster import DBSCAN, AgglomerativeClustering from tqdm import tqdm + from stlearn.pl import het_plot diff --git a/stlearn/tools/microenv/cci/het.py b/stlearn/tools/microenv/cci/het.py index 60e6e1bb..8ed58b79 100644 --- a/stlearn/tools/microenv/cci/het.py +++ b/stlearn/tools/microenv/cci/het.py @@ -1,4 +1,4 @@ -from typing import Iterable +from collections.abc import Iterable import numpy as np import pandas as pd @@ -416,7 +416,7 @@ def create_grids(adata: AnnData, num_row: int, num_col: int, radius: int = 1): grids, neighbours = [], [] # generate grids from top to bottom and left to right for n in range(num_row * num_col): - neighbour: Iterable[float] + neighbour: Iterable[float] = [] x = min_x + n // num_row * width # left side y = min_y + n % num_row * height # upper side grids.append([x, y]) diff --git a/stlearn/utils.py b/stlearn/utils.py index a9b5cb38..fb03fcf1 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -88,7 +88,8 @@ def _check_img( Parameters ---------- img : np.ndarray | None - If given an image will not look for another image and not check to see if it was in spatial_data. + If given an image will not look for another image and not check to see if it + was in spatial_data. img_key : None | str | Empty If None - don't find an image. Empty - find best image, or specify with str. @@ -108,7 +109,8 @@ def _check_img( new_img_key: str | None = None new_img: np.ndarray | None = None - # Return the img if not None and convert the key to Empty -> None if Empty otherwise keep. + # Return the img if not None and convert the key to Empty -> None if Empty + # otherwise keep. if img is not None: new_img = img new_img_key = img_key if img_key is not _empty else None diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 291dd138..a3faac59 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -2,9 +2,10 @@ import json import logging as logg +from collections.abc import Iterator from os import PathLike from pathlib import Path -from typing import Iterator, Literal +from typing import Literal import matplotlib.pyplot as plt import numpy as np From eb12b699f226de3b99b603e911964896ebe61e0c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 6 Jun 2025 15:00:04 +1000 Subject: [PATCH 046/241] Reformat. --- stlearn/__main__.py | 1 - stlearn/adds/add_mask.py | 4 ++- stlearn/app/app.py | 2 -- stlearn/app/source/forms/form_validators.py | 1 - stlearn/app/source/forms/utils.py | 1 - stlearn/app/source/forms/views.py | 1 - stlearn/classes.py | 1 - stlearn/logging.py | 2 +- stlearn/plotting/cci_plot.py | 4 +-- stlearn/plotting/cci_plot_helpers.py | 4 +-- stlearn/plotting/classes.py | 29 +++---------------- stlearn/plotting/classes_bokeh.py | 9 ------ stlearn/plotting/gene_plot.py | 1 - stlearn/plotting/non_spatial_plot.py | 1 - stlearn/plotting/stack_3d_plot.py | 6 ++-- stlearn/plotting/subcluster_plot.py | 6 ++-- .../plotting/trajectory/check_trajectory.py | 18 ++++++------ .../plotting/trajectory/pseudotime_plot.py | 3 -- stlearn/spatials/SME/_weighting_matrix.py | 1 - stlearn/spatials/clustering/localization.py | 1 - stlearn/spatials/smooth/disk.py | 1 - stlearn/spatials/trajectory/utils.py | 3 +- stlearn/tools/microenv/cci/analysis.py | 16 +++++----- stlearn/tools/microenv/cci/base.py | 2 +- stlearn/tools/microenv/cci/het.py | 1 - stlearn/tools/microenv/cci/het_helpers.py | 2 -- stlearn/tools/microenv/cci/perm_utils.py | 10 +++++-- stlearn/tools/microenv/cci/permutation.py | 3 +- stlearn/wrapper/concatenate_spatial_adata.py | 1 - stlearn/wrapper/convert_scanpy.py | 1 - stlearn/wrapper/read.py | 27 +++++++++-------- tests/test_PSTS.py | 1 - tests/test_SME.py | 1 - tox.ini | 2 +- 34 files changed, 59 insertions(+), 108 deletions(-) diff --git a/stlearn/__main__.py b/stlearn/__main__.py index 3802ae27..43559dfc 100644 --- a/stlearn/__main__.py +++ b/stlearn/__main__.py @@ -2,7 +2,6 @@ """Package entry point.""" - from stlearn.app import cli if __name__ == "__main__": # pragma: no cover diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index 998b4936..84c24c4a 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -49,8 +49,10 @@ def add_mask( img = plt.imread(imgpath, 0) assert ( img.shape == adata.uns["spatial"][library_id]["images"][quality].shape - ), "\ + ), ( + "\ size of mask image does not match size of H&E images" + ) if "mask_image" not in adata.uns: adata.uns["mask_image"] = {} if library_id not in adata.uns["mask_image"]: diff --git a/stlearn/app/app.py b/stlearn/app/app.py index 1e393468..25964ae7 100644 --- a/stlearn/app/app.py +++ b/stlearn/app/app.py @@ -154,7 +154,6 @@ def folder_uploader(): uploaded = [] i = 0 for file in files: - filename = secure_filename(file.filename) if allow_files[0] in filename: @@ -226,7 +225,6 @@ def folder_uploader(): @app.route("/file_uploader", methods=["GET", "POST"]) def file_uploader(): if request.method == "POST": - global adata, step_log # Clean uploads folder before upload a new data diff --git a/stlearn/app/source/forms/form_validators.py b/stlearn/app/source/forms/form_validators.py index 1d6f2672..4a279164 100644 --- a/stlearn/app/source/forms/form_validators.py +++ b/stlearn/app/source/forms/form_validators.py @@ -10,7 +10,6 @@ def __init__(self, lower, upper, hint=""): self.hint = hint def __call__(self, form, field): - if field.data is not None: if not (self.lower <= float(field.data) <= self.upper): if self.hint: diff --git a/stlearn/app/source/forms/utils.py b/stlearn/app/source/forms/utils.py index 1d5d0c75..42121bcf 100644 --- a/stlearn/app/source/forms/utils.py +++ b/stlearn/app/source/forms/utils.py @@ -11,7 +11,6 @@ def flash_errors(form, category="warning"): def get_all_paths(adata): - import networkx as nx G = nx.from_numpy_array(adata.uns["paga"]["connectivities_tree"].toarray()) diff --git a/stlearn/app/source/forms/views.py b/stlearn/app/source/forms/views.py index 3dbeed18..3aa58bbb 100644 --- a/stlearn/app/source/forms/views.py +++ b/stlearn/app/source/forms/views.py @@ -367,7 +367,6 @@ def run_dea(request, adata, step_log): else: try: - sc.tl.rank_genes_groups(adata, element_values[0], method=element_values[1]) step_log["dea"][0] = True diff --git a/stlearn/classes.py b/stlearn/classes.py index f441661b..27e1322e 100644 --- a/stlearn/classes.py +++ b/stlearn/classes.py @@ -35,7 +35,6 @@ def __init__( use_raw: bool = False, **kwargs, ): - self.adata = (adata,) self.library_id, self.spatial_data = _check_spatial_data(adata.uns, library_id) self.img, self.img_key = _check_img(self.spatial_data, img, img_key, bw=bw) diff --git a/stlearn/logging.py b/stlearn/logging.py index be37ad1f..6b4d0ce8 100644 --- a/stlearn/logging.py +++ b/stlearn/logging.py @@ -280,7 +280,7 @@ def print_version_and_date(): from ._settings import settings print( - f"Running Scanpy {__version__}, " f"on {datetime.now():%Y-%m-%d %H:%M}.", + f"Running Scanpy {__version__}, on {datetime.now():%Y-%m-%d %H:%M}.", file=settings.logfile, ) diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 0f8b0d5d..1e544486 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -667,7 +667,7 @@ def lr_plot( elif sig_spots and not lr_sig: raise Exception( - "LR has no significant spots, to visualise anyhow set" "sig_spots=False" + "LR has no significant spots, to visualise anyhow setsig_spots=False" ) # Making sure have run_cci first with respective labelling # @@ -713,7 +713,7 @@ def lr_plot( and use_label not in lr_use_labels ): raise Exception( - f"use_label must be in adata.obs or " f"one of lr stats: {lr_use_labels}." + f"use_label must be in adata.obs or one of lr stats: {lr_use_labels}." ) out_options = ["binary", "continuous", None] diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index b94f147f..4706b3a2 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -1,6 +1,5 @@ """Helper functions for cci_plot.py.""" - import matplotlib import matplotlib.cm as cm import matplotlib.colors as plt_colors @@ -48,7 +47,7 @@ def lr_scatter( lr_features = data.uns["lrfeatures"] lr_df = pd.concat([lr_df, lr_features], axis=1).loc[lrs, :] if feature not in lr_df.columns: - raise Exception(f"Inputted {feature}; must be one of " f"{list(lr_df.columns)}") + raise Exception(f"Inputted {feature}; must be one of {list(lr_df.columns)}") rot = 90 if feature != "n_spots_sig" else 70 @@ -426,7 +425,6 @@ def get_int_df(adata, lr, use_label, sig_interactions, title): )[labels_ordered].loc[labels_ordered] title = "Cell-Cell LR Interactions" if no_title else title else: - labels_ordered = adata.obs[use_label].cat.categories int_df = ( adata.uns[f"per_lr_cci_{use_label}"][lr] diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index d40fcb2d..4772f2a6 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -71,14 +71,12 @@ def __init__( assert use_label is not None, "Please specify `use_label` parameter!" if use_label is not None: - - assert ( - use_label in self.adata[0].obs.columns - ), "Please choose the right label in `adata.obs.columns`!" + assert use_label in self.adata[0].obs.columns, ( + "Please choose the right label in `adata.obs.columns`!" + ) self.use_label = use_label if self.list_clusters is None: - self.list_clusters = np.array( self.adata[0].obs[use_label].cat.categories ) @@ -166,7 +164,6 @@ def _add_image(self, main_ax: Axes): ) def _plot_colorbar(self, plot_ax: Axes, color_bar_label: str = ""): - cb = plt.colorbar( plot_ax, aspect=10, shrink=0.5, cmap=self.cmap, label=color_bar_label ) @@ -176,7 +173,6 @@ def _remove_axis(self, main_ax: Axes): main_ax.axis("off") def _crop_image(self, main_ax: _AxesSubplot, margin: float): - main_ax.set_xlim(self.imagecol.min() - margin, self.imagecol.max() + margin) main_ax.set_ylim(self.imagerow.min() - margin, self.imagerow.max() + margin) main_ax.set_ylim(main_ax.get_ylim()[::-1]) @@ -184,7 +180,6 @@ def _crop_image(self, main_ax: _AxesSubplot, margin: float): def _zoom_image( self, main_ax: _AxesSubplot, zoom_coord: tuple[float, float, float, float] ): - main_ax.set_xlim(zoom_coord[0], zoom_coord[1]) main_ax.set_ylim(zoom_coord[3], zoom_coord[2]) @@ -211,7 +206,6 @@ def _get_query_clusters_index(self): return index_query def _save_output(self): - self.fig.savefig( fname=self.fname, bbox_inches="tight", pad_inches=0, dpi=self.dpi ) @@ -324,13 +318,11 @@ def __init__( self._save_output() def _get_gene_expression(self): - # Gene plot option if len(self.gene_symbols) == 0: raise ValueError("Genes should be provided, please input genes") elif len(self.gene_symbols) == 1: - if self.gene_symbols[0] not in self.query_adata.var_names: raise ValueError( self.gene_symbols[0] @@ -341,7 +333,6 @@ def _get_gene_expression(self): return colors else: - for gene in self.gene_symbols: if gene not in self.query_adata.var.index: self.gene_symbols.remove(gene) @@ -371,7 +362,6 @@ def _get_gene_expression(self): return colors def _plot_genes(self, gene_values: pd.Series): - if self.vmin is None and self.vmax is None: vmin = min(gene_values) vmax = max(gene_values) @@ -396,7 +386,6 @@ def _plot_genes(self, gene_values: pd.Series): return plot def _plot_contour(self, gene_values: pd.Series): - imgcol_new = self.query_adata.obsm["spatial"][:, 0] * self.scale_factor imgrow_new = self.query_adata.obsm["spatial"][:, 1] * self.scale_factor # Extracting x,y and values (z) @@ -523,7 +512,6 @@ def __init__( self._save_output() def _get_feature_values(self): - if self.feature not in self.query_adata.obs: raise ValueError( self.feature + " is not in data.obs, please try another feature" @@ -541,7 +529,6 @@ def _get_feature_values(self): return colors def _plot_feature(self, feature_values: pd.Series): - if self.vmin is None and self.vmax is None: vmin = min(feature_values) vmax = max(feature_values) @@ -566,7 +553,6 @@ def _plot_feature(self, feature_values: pd.Series): return plot def _plot_contour(self, feature_values: pd.Series): - imgcol_new = self.query_adata.obsm["spatial"][:, 0] * self.scale_factor imgrow_new = self.query_adata.obsm["spatial"][:, 1] * self.scale_factor # Extracting x,y and values (z) @@ -715,7 +701,6 @@ def _plot_clusters(self): # Plot scatter plot based on pixel of spots for i, cluster in enumerate(self.query_adata.obs.groupby(self.use_label)): - # Plot scatter plot based on pixel of spots subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), list(cluster[1].index)) @@ -761,9 +746,7 @@ def _add_cluster_bar(self, bbox_to_anchor): handle.set_sizes([20.0]) def _add_cluster_labels(self): - for i, label in enumerate(self.list_clusters): - label_index = list( self.query_adata.obs[ self.query_adata.obs[self.use_label] == str(label) @@ -802,7 +785,6 @@ def _add_cluster_labels(self): ) def _add_sub_clusters(self): - if "sub_cluster_labels" not in self.query_adata.obs.columns: raise ValueError("Please run stlearn.spatial.cluster.localization") @@ -926,7 +908,6 @@ def _add_trajectories(self): if self.show_node: for x, y in centroid_dict.items(): - if x in get_node(self.list_clusters, self.adata[0].uns["split_node"]): self.ax.text( y[0], @@ -1221,9 +1202,7 @@ def __init__( if use_lr is None: use_lr = adata.uns["lr_summary"].index.values[0] elif use_lr not in adata.uns["lr_summary"].index: - raise Exception( - f"use_lr must be one of:\n" f'{adata.uns["lr_summary"].index}' - ) + raise Exception(f"use_lr must be one of:\n{adata.uns['lr_summary'].index}") else: use_lr = str(use_lr) diff --git a/stlearn/plotting/classes_bokeh.py b/stlearn/plotting/classes_bokeh.py index 9b9e8ee9..54a69c0b 100644 --- a/stlearn/plotting/classes_bokeh.py +++ b/stlearn/plotting/classes_bokeh.py @@ -136,7 +136,6 @@ def __init__( # self.tab = Tabs(tabs = [Panel(child=self.layout, title="Gene plot")]) def modify_fig(doc): - doc.add_root(row(self.layout, width=800)) self.data_alpha.on_change("value", self.update_data) @@ -153,7 +152,6 @@ def modify_fig(doc): self.app = Application(handler) def make_fig(self): - fig = figure( title=self.gene_select.value, x_range=(0, self.dim), @@ -269,7 +267,6 @@ def add_violin(self): return p def update_data(self, attrname, old, new): - if len(self.menu) != 0: self.layout.children[0].children[1] = self.make_fig() self.layout.children[1] = self.add_violin() @@ -277,7 +274,6 @@ def update_data(self, attrname, old, new): self.layout.children[1] = self.make_fig() def _get_gene_expression(self, gene_symbols): - if gene_symbols[0] not in self.adata[0].var_names: raise ValueError( gene_symbols[0] + " is not exist in the data, please try another gene" @@ -508,7 +504,6 @@ def modify_fig(doc): self.app = Application(handler) def update_list(self, attrname, old, name): - # Initialize the color from stlearn.plotting.cluster_plot import cluster_plot @@ -521,7 +516,6 @@ def update_list(self, attrname, old, name): ) def update_data(self, attrname, old, new): - if "rank_genes_groups" in self.adata[0].uns: if ( self.use_label.value @@ -862,7 +856,6 @@ def modify_fig(doc): self.app = Application(handler) def make_fig(self): - fig = figure( title=self.lr_select.value, # self.het_select.value, x_range=(0, self.dim - 150), @@ -929,7 +922,6 @@ def update_data(self, attrname, old, new): self.layout.children[1] = self.make_fig() def _get_het(self, het): - if het not in self.adata[0].obsm: raise ValueError(het + " is not exist in the data, please try another het") @@ -1058,7 +1050,6 @@ def modify_fig(doc): self.app = Application(handler) def make_fig(self): - fig = figure( title="Spatial CCI plot", x_range=(0, self.dim - 150), diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index f7f770e0..cca713d0 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -1,4 +1,3 @@ - import matplotlib from anndata import AnnData from bokeh.io import output_notebook diff --git a/stlearn/plotting/non_spatial_plot.py b/stlearn/plotting/non_spatial_plot.py index 800d5779..dcdf2307 100644 --- a/stlearn/plotting/non_spatial_plot.py +++ b/stlearn/plotting/non_spatial_plot.py @@ -45,7 +45,6 @@ def non_spatial_plot( scanpy.pl.draw_graph(adata, color="dpt_pseudotime") else: - scanpy.pl.draw_graph(adata) # adata.uns[use_label+"_colors"] = adata.uns["tmp_color"] diff --git a/stlearn/plotting/stack_3d_plot.py b/stlearn/plotting/stack_3d_plot.py index c958575a..0bc23896 100644 --- a/stlearn/plotting/stack_3d_plot.py +++ b/stlearn/plotting/stack_3d_plot.py @@ -43,9 +43,9 @@ def stack_3d_plot( except ModuleNotFoundError: raise ModuleNotFoundError("Please install plotly by `pip install plotly`") - assert ( - slide_col in adata.obs.columns - ), "Please provide the right column for slide_id!" + assert slide_col in adata.obs.columns, ( + "Please provide the right column for slide_id!" + ) list_df = [] for i, slide in enumerate(slides): diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/plotting/subcluster_plot.py index 0e8c66e5..1763c716 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/plotting/subcluster_plot.py @@ -58,9 +58,9 @@ def subcluster_plot( """ assert use_label is not None, "Please select `use_label` parameter" - assert ( - use_label in adata.obs.columns - ), "Please run `stlearn.spatial.cluster.localization` function!" + assert use_label in adata.obs.columns, ( + "Please run `stlearn.spatial.cluster.localization` function!" + ) SubClusterPlot( adata, diff --git a/stlearn/plotting/trajectory/check_trajectory.py b/stlearn/plotting/trajectory/check_trajectory.py index 587c20e9..2699d2a0 100644 --- a/stlearn/plotting/trajectory/check_trajectory.py +++ b/stlearn/plotting/trajectory/check_trajectory.py @@ -17,16 +17,16 @@ def check_trajectory( img_key: str = "hires", ) -> None: trajectory = np.array(trajectory).astype(int) - assert ( - trajectory in adata.uns["available_paths"].values() - ), "Please choose the right path!" + assert trajectory in adata.uns["available_paths"].values(), ( + "Please choose the right path!" + ) trajectory_str = [str(node) for node in trajectory] - assert ( - pseudotime_key in adata.obs.columns - ), "Please run the pseudotime or choose the right one!" - assert ( - use_label in adata.obs.columns - ), "Please run the cluster or choose the right label!" + assert pseudotime_key in adata.obs.columns, ( + "Please run the pseudotime or choose the right one!" + ) + assert use_label in adata.obs.columns, ( + "Please run the cluster or choose the right label!" + ) assert basis in adata.obsm, ( "Please run the " + basis + "before you check the trajectory!" ) diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index b6d3a167..4ee2a115 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -153,7 +153,6 @@ def pseudotime_plot( ) for x, y in centroid_dict.items(): - if x in get_node(list_clusters, adata.uns["split_node"]): a.text( y[0], @@ -173,7 +172,6 @@ def pseudotime_plot( ) if show_trajectories: - used_colors = adata.uns[use_label + "_colors"] cmaps = matplotlib.colors.LinearSegmentedColormap.from_list("", used_colors) @@ -218,7 +216,6 @@ def pseudotime_plot( if show_node: for x, y in centroid_dict.items(): - if x in get_node(list_clusters, adata.uns["split_node"]): a.text( y[0], diff --git a/stlearn/spatials/SME/_weighting_matrix.py b/stlearn/spatials/SME/_weighting_matrix.py index 2ff23eb4..12848161 100644 --- a/stlearn/spatials/SME/_weighting_matrix.py +++ b/stlearn/spatials/SME/_weighting_matrix.py @@ -126,7 +126,6 @@ def impute_neighbour( bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(len(coor)): - main_weights = weights_matrix[i] if weights == "physical_distance": diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index efc1774f..1fd17129 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -44,7 +44,6 @@ def localization( pd.set_option("mode.chained_assignment", None) subclusters_list = [] for i in adata.obs[use_label].unique(): - tmp = adata.obs[adata.obs[use_label] == i] clustering = DBSCAN(eps=eps, min_samples=1, algorithm="kd_tree").fit( diff --git a/stlearn/spatials/smooth/disk.py b/stlearn/spatials/smooth/disk.py index 01ff2309..a259aee4 100644 --- a/stlearn/spatials/smooth/disk.py +++ b/stlearn/spatials/smooth/disk.py @@ -11,7 +11,6 @@ def disk( method: str = "mean", copy: bool = False, ) -> AnnData | None: - adata = adata.copy() if copy else adata coor = adata.obs[["imagecol", "imagerow"]] diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatials/trajectory/utils.py index 46d700fb..2490b9d7 100644 --- a/stlearn/spatials/trajectory/utils.py +++ b/stlearn/spatials/trajectory/utils.py @@ -1,4 +1,3 @@ - import networkx as nx import numpy as np from numpy import linalg as la @@ -359,7 +358,7 @@ def resistance_matrix(A, check_connected=True): G = nx.from_numpy_array(A) if not nx.is_connected(G): raise UndefinedException( - "Graph is not connected. " "Resistance matrix is undefined." + "Graph is not connected. Resistance matrix is undefined." ) L = laplacian_matrix(A) try: diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index 1758f154..5318ac08 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -51,7 +51,7 @@ def load_lrs(names: str | list | None = None, species: str = "human") -> np.ndar dbs = [pd.read_csv(f"{path}/databases/{name}.txt", sep="\t") for name in names] lrs_full = [] for db in dbs: - lrs = [f"{db.values[i,0]}_{db.values[i,1]}" for i in range(db.shape[0])] + lrs = [f"{db.values[i, 0]}_{db.values[i, 1]}" for i in range(db.shape[0])] lrs_full.extend(lrs) lrs_full_arr = np.unique(np.array(lrs_full)) # If dealing with mouse, need to reformat # @@ -112,9 +112,10 @@ def grid( # Retrieving the coordinates of each grid # n_squares = n_row * n_col cell_bcs = adata.obs_names.values.astype(str) - xs, ys = adata.obs["imagecol"].values.astype(int), adata.obs[ - "imagerow" - ].values.astype(int) + xs, ys = ( + adata.obs["imagecol"].values.astype(int), + adata.obs["imagerow"].values.astype(int), + ) grid_counts, xedges, yedges = np.histogram2d(xs, ys, bins=[n_col, n_row]) grid_counts, xedges, yedges = ( @@ -484,7 +485,7 @@ def run_lr_go( # Making sure inputted correct species all_species = ["human", "mouse"] if species not in all_species: - raise Exception(f"Got {species} for species, must be one of " f"{all_species}") + raise Exception(f"Got {species} for species, must be one of {all_species}") # Getting the genes from the top LR pairs if "lr_summary" not in adata.uns: @@ -604,7 +605,7 @@ def run_cci( ran_sig = False if not ran_lr else "n_spots_sig" in adata.uns["lr_summary"].columns if not ran_lr and not ran_sig: raise Exception( - "No LR results testing results found, " "please run st.tl.cci.run first" + "No LR results testing results found, please run st.tl.cci.run first" ) # Ensuring compatibility with current way of adding label_transfer to object @@ -616,8 +617,7 @@ def run_cci( # Getting the cell/tissue types that we are actually testing # if obs_key not in adata.obs: raise Exception( - f"Missing {obs_key} from adata.obs, need this even if " - f"using mixture mode." + f"Missing {obs_key} from adata.obs, need this even if using mixture mode." ) tissue_types = adata.obs[obs_key].values.astype(str) all_set = np.unique(tissue_types) diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tools/microenv/cci/base.py index c72b2db3..2b668728 100644 --- a/stlearn/tools/microenv/cci/base.py +++ b/stlearn/tools/microenv/cci/base.py @@ -181,7 +181,7 @@ def get_spot_lrs( l1, ... rn, ln """ df = adata.to_df() - pairs_rev = [f'{pair.split("_")[1]}_{pair.split("_")[0]}' for pair in lr_pairs] + pairs_rev = [f"{pair.split('_')[1]}_{pair.split('_')[0]}" for pair in lr_pairs] pairs_wRev = [] for i in range(len(lr_pairs)): pairs_wRev.extend([lr_pairs[i], pairs_rev[i]]) diff --git a/stlearn/tools/microenv/cci/het.py b/stlearn/tools/microenv/cci/het.py index 8ed58b79..c558a441 100644 --- a/stlearn/tools/microenv/cci/het.py +++ b/stlearn/tools/microenv/cci/het.py @@ -356,7 +356,6 @@ def get_interactions( # Now retrieving the interaction edges # for i in range(all_set.shape[0]): - # Determining which spots have cell type A # A_bool_2 = cell_data[:, i] > cell_prop_cutoff A_gene1_bool = np.logical_and(A_bool_2, gene1_bool) diff --git a/stlearn/tools/microenv/cci/het_helpers.py b/stlearn/tools/microenv/cci/het_helpers.py index 3c8dc935..e5761f15 100644 --- a/stlearn/tools/microenv/cci/het_helpers.py +++ b/stlearn/tools/microenv/cci/het_helpers.py @@ -245,7 +245,6 @@ def get_neighbourhoods_FAST( neigh_bcs_array, neigh_indices = [], [] for j, neigh_bc in enumerate(neigh_bcs): - bc_indices = np.where(spot_bcs == neigh_bc)[0] if len(bc_indices) > 0: neigh_bcs_array.append(neigh_bc) @@ -285,7 +284,6 @@ def get_neighbourhoods(adata): neighbourhood_indices.append((spot_i, neighbours[spot_i])) neighbourhood_bcs.append((spot_bcs[spot_i], spot_bcs[neighbours[spot_i]])) else: # Newer version - spot_bcs = adata.obs_names.values.astype(str) spot_neigh_bcs = adata.obsm["spot_neigh_bcs"].values.astype(str) diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index 63b048b8..869e5894 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -355,7 +355,10 @@ def get_lr_bg( l_, r_ = lr_.split("_") if l_ not in gene_bg_genes: l_genes = get_similar_genesFAST( - l_quant, n_genes, candidate_quants, genes # group_l_props, + l_quant, + n_genes, + candidate_quants, + genes, # group_l_props, ) gene_bg_genes[l_] = l_genes else: @@ -363,7 +366,10 @@ def get_lr_bg( if r_ not in gene_bg_genes: r_genes = get_similar_genesFAST( - r_quant, n_genes, candidate_quants, genes # group_r_props, + r_quant, + n_genes, + candidate_quants, + genes, # group_r_props, ) gene_bg_genes[r_] = r_genes else: diff --git a/stlearn/tools/microenv/cci/permutation.py b/stlearn/tools/microenv/cci/permutation.py index 46491b0b..60bd9bd3 100644 --- a/stlearn/tools/microenv/cci/permutation.py +++ b/stlearn/tools/microenv/cci/permutation.py @@ -47,8 +47,7 @@ def perform_spot_testing( n_genes = round(np.sqrt(n_pairs) * 2) if len(genes) < n_genes: print( - "Exiting since need atleast " - f"{n_genes} genes to generate {n_pairs} pairs." + f"Exiting since need atleast {n_genes} genes to generate {n_pairs} pairs." ) return diff --git a/stlearn/wrapper/concatenate_spatial_adata.py b/stlearn/wrapper/concatenate_spatial_adata.py index cc9273d7..a1c8b8ce 100644 --- a/stlearn/wrapper/concatenate_spatial_adata.py +++ b/stlearn/wrapper/concatenate_spatial_adata.py @@ -15,7 +15,6 @@ def transform_spatial(coordinates, original, resized): def correct_size(adata, fixed_size): - image = adata.uns["spatial"][list(adata.uns["spatial"].keys())[0]]["images"][ "hires" ] diff --git a/stlearn/wrapper/convert_scanpy.py b/stlearn/wrapper/convert_scanpy.py index 94a74ded..aac9c6a9 100644 --- a/stlearn/wrapper/convert_scanpy.py +++ b/stlearn/wrapper/convert_scanpy.py @@ -5,7 +5,6 @@ def convert_scanpy( adata: AnnData, use_quality: str = "hires", ) -> AnnData | None: - adata.var_names_make_unique() library_id = list(adata.uns["spatial"].keys())[0] diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index a3faac59..409b45cc 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -121,8 +121,7 @@ def Read10X( if not f.exists(): if any(x in str(f) for x in ["hires_image", "lowres_image"]): logg.warning( - f"You seem to be missing an image file.\n" - f"Could not find '{f}'." + f"You seem to be missing an image file.\nCould not find '{f}'." ) else: raise OSError(f"Could not find '{f}'") @@ -329,9 +328,9 @@ def ReadSlideSeq( "tissue_" + quality + "_scalef" ] = scale - adata.uns["spatial"][library_id]["scalefactors"][ - "spot_diameter_fullres" - ] = spot_diameter_fullres + adata.uns["spatial"][library_id]["scalefactors"]["spot_diameter_fullres"] = ( + spot_diameter_fullres + ) adata.obsm["spatial"] = meta[["x", "y"]].values return adata @@ -501,9 +500,9 @@ def ReadSeqFish( adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" ] = scale - adata.uns["spatial"][library_id]["scalefactors"][ - "spot_diameter_fullres" - ] = spot_diameter_fullres + adata.uns["spatial"][library_id]["scalefactors"]["spot_diameter_fullres"] = ( + spot_diameter_fullres + ) return adata @@ -594,9 +593,9 @@ def ReadXenium( adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" ] = scale - adata.uns["spatial"][library_id]["scalefactors"][ - "spot_diameter_fullres" - ] = spot_diameter_fullres + adata.uns["spatial"][library_id]["scalefactors"]["spot_diameter_fullres"] = ( + spot_diameter_fullres + ) return adata @@ -676,8 +675,8 @@ def create_stlearn( adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" ] = scale - adata.uns["spatial"][library_id]["scalefactors"][ - "spot_diameter_fullres" - ] = spot_diameter_fullres + adata.uns["spatial"][library_id]["scalefactors"]["spot_diameter_fullres"] = ( + spot_diameter_fullres + ) return adata diff --git a/tests/test_PSTS.py b/tests/test_PSTS.py index 21089a6a..08ceba53 100644 --- a/tests/test_PSTS.py +++ b/tests/test_PSTS.py @@ -2,7 +2,6 @@ """Tests for `stlearn` package.""" - import unittest import numpy as np diff --git a/tests/test_SME.py b/tests/test_SME.py index a1f38200..49ea98ec 100644 --- a/tests/test_SME.py +++ b/tests/test_SME.py @@ -2,7 +2,6 @@ """Tests for `stlearn` package.""" - import unittest import scanpy as sc diff --git a/tox.ini b/tox.ini index 76e1b229..984d1e61 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] requires = tox>=4 -env_list = lint, type, 3.1{3,2,1,0}, ruff +env_list = lint, type, 3.10, ruff [testenv:lint] description = run linters From 6e4a45cc2cdc400041bebdd6174cc2dc1994c291 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 6 Jun 2025 15:11:32 +1000 Subject: [PATCH 047/241] Reformat. --- stlearn/adds/add_mask.py | 4 +--- stlearn/plotting/classes.py | 6 ++--- stlearn/plotting/stack_3d_plot.py | 6 ++--- stlearn/plotting/subcluster_plot.py | 6 ++--- .../plotting/trajectory/check_trajectory.py | 18 +++++++------- stlearn/wrapper/read.py | 24 +++++++++---------- tox.ini | 1 - 7 files changed, 31 insertions(+), 34 deletions(-) diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index 84c24c4a..998b4936 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -49,10 +49,8 @@ def add_mask( img = plt.imread(imgpath, 0) assert ( img.shape == adata.uns["spatial"][library_id]["images"][quality].shape - ), ( - "\ + ), "\ size of mask image does not match size of H&E images" - ) if "mask_image" not in adata.uns: adata.uns["mask_image"] = {} if library_id not in adata.uns["mask_image"]: diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 4772f2a6..6c21f832 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -71,9 +71,9 @@ def __init__( assert use_label is not None, "Please specify `use_label` parameter!" if use_label is not None: - assert use_label in self.adata[0].obs.columns, ( - "Please choose the right label in `adata.obs.columns`!" - ) + assert ( + use_label in self.adata[0].obs.columns + ), "Please choose the right label in `adata.obs.columns`!" self.use_label = use_label if self.list_clusters is None: diff --git a/stlearn/plotting/stack_3d_plot.py b/stlearn/plotting/stack_3d_plot.py index 0bc23896..c958575a 100644 --- a/stlearn/plotting/stack_3d_plot.py +++ b/stlearn/plotting/stack_3d_plot.py @@ -43,9 +43,9 @@ def stack_3d_plot( except ModuleNotFoundError: raise ModuleNotFoundError("Please install plotly by `pip install plotly`") - assert slide_col in adata.obs.columns, ( - "Please provide the right column for slide_id!" - ) + assert ( + slide_col in adata.obs.columns + ), "Please provide the right column for slide_id!" list_df = [] for i, slide in enumerate(slides): diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/plotting/subcluster_plot.py index 1763c716..0e8c66e5 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/plotting/subcluster_plot.py @@ -58,9 +58,9 @@ def subcluster_plot( """ assert use_label is not None, "Please select `use_label` parameter" - assert use_label in adata.obs.columns, ( - "Please run `stlearn.spatial.cluster.localization` function!" - ) + assert ( + use_label in adata.obs.columns + ), "Please run `stlearn.spatial.cluster.localization` function!" SubClusterPlot( adata, diff --git a/stlearn/plotting/trajectory/check_trajectory.py b/stlearn/plotting/trajectory/check_trajectory.py index 2699d2a0..587c20e9 100644 --- a/stlearn/plotting/trajectory/check_trajectory.py +++ b/stlearn/plotting/trajectory/check_trajectory.py @@ -17,16 +17,16 @@ def check_trajectory( img_key: str = "hires", ) -> None: trajectory = np.array(trajectory).astype(int) - assert trajectory in adata.uns["available_paths"].values(), ( - "Please choose the right path!" - ) + assert ( + trajectory in adata.uns["available_paths"].values() + ), "Please choose the right path!" trajectory_str = [str(node) for node in trajectory] - assert pseudotime_key in adata.obs.columns, ( - "Please run the pseudotime or choose the right one!" - ) - assert use_label in adata.obs.columns, ( - "Please run the cluster or choose the right label!" - ) + assert ( + pseudotime_key in adata.obs.columns + ), "Please run the pseudotime or choose the right one!" + assert ( + use_label in adata.obs.columns + ), "Please run the cluster or choose the right label!" assert basis in adata.obsm, ( "Please run the " + basis + "before you check the trajectory!" ) diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 409b45cc..730666c2 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -328,9 +328,9 @@ def ReadSlideSeq( "tissue_" + quality + "_scalef" ] = scale - adata.uns["spatial"][library_id]["scalefactors"]["spot_diameter_fullres"] = ( - spot_diameter_fullres - ) + adata.uns["spatial"][library_id]["scalefactors"][ + "spot_diameter_fullres" + ] = spot_diameter_fullres adata.obsm["spatial"] = meta[["x", "y"]].values return adata @@ -500,9 +500,9 @@ def ReadSeqFish( adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" ] = scale - adata.uns["spatial"][library_id]["scalefactors"]["spot_diameter_fullres"] = ( - spot_diameter_fullres - ) + adata.uns["spatial"][library_id]["scalefactors"][ + "spot_diameter_fullres" + ] = spot_diameter_fullres return adata @@ -593,9 +593,9 @@ def ReadXenium( adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" ] = scale - adata.uns["spatial"][library_id]["scalefactors"]["spot_diameter_fullres"] = ( - spot_diameter_fullres - ) + adata.uns["spatial"][library_id]["scalefactors"][ + "spot_diameter_fullres" + ] = spot_diameter_fullres return adata @@ -675,8 +675,8 @@ def create_stlearn( adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" ] = scale - adata.uns["spatial"][library_id]["scalefactors"]["spot_diameter_fullres"] = ( - spot_diameter_fullres - ) + adata.uns["spatial"][library_id]["scalefactors"][ + "spot_diameter_fullres" + ] = spot_diameter_fullres return adata diff --git a/tox.ini b/tox.ini index 984d1e61..dcdf7115 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,6 @@ skip_install = true deps = ruff commands = ruff check stlearn tests - ruff format --check stlearn tests [testenv] setenv = From 34e636e5ff913c8ddd28b56a7871ac7a186224d3 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 9 Jun 2025 09:54:41 +1000 Subject: [PATCH 048/241] Upgrade numba and numpy. --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a1f23831..4c11dc46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ bokeh==3.7.3 click==8.2.1 leidenalg==0.10.2 louvain==0.8.2 -numba==0.56.4 -numpy==1.23.5 +numba==0.58.1 +numpy==1.26.4 pillow==11.2.1 scanpy==1.10.4 scikit-image==0.22.0 From 67c999db0a6b1eaa2f1428d1e55144ba6ea3cae6 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 9 Jun 2025 10:04:59 +1000 Subject: [PATCH 049/241] WIP. --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f6f1d45e..b9769b45 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -66,7 +66,7 @@ Ready to contribute? Here's how to set up `stlearn` for local development. 3. Install your local copy into a virtualenv. This is how you set up your fork for local development:: - $ conda create -n stlearn-dev python=3.10 + $ conda create -n stlearn-dev python=3.10 --y $ conda activate stlearn-dev $ cd stlearn/ $ pip install -e .[dev,test] From 3ed0539f22bce232cf15a4b096183b9fab2c7a20 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 9 Jun 2025 16:45:09 +1000 Subject: [PATCH 050/241] Add tests and fix bug. --- stlearn/classes.py | 2 +- stlearn/utils.py | 5 +++++ tests/test_Spatial.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/test_Spatial.py diff --git a/stlearn/classes.py b/stlearn/classes.py index 27e1322e..b131c0d6 100644 --- a/stlearn/classes.py +++ b/stlearn/classes.py @@ -27,7 +27,7 @@ def __init__( basis: str = "spatial", img: np.ndarray | None = None, img_key: str | None | Empty = _empty, - library_id: str | None = None, + library_id: str | None | Empty = _empty, crop_coord: bool = True, bw: bool = False, scale_factor: float | None = None, diff --git a/stlearn/utils.py b/stlearn/utils.py index fb03fcf1..6845a23c 100644 --- a/stlearn/utils.py +++ b/stlearn/utils.py @@ -57,6 +57,11 @@ def _check_spatial_data( """ Given a mapping, try and extract a library id/ mapping with spatial data. Assumes this is `.uns` from how we parse visium data. + + Parameters + ---------- + library_id : None | str | Empty + If None - don't find an image. Empty - find best image, or specify with str. """ spatial_mapping = uns.get("spatial", {}) if library_id is _empty: diff --git a/tests/test_Spatial.py b/tests/test_Spatial.py new file mode 100644 index 00000000..b31fa8cd --- /dev/null +++ b/tests/test_Spatial.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +"""Tests for `stlearn` package.""" + +import unittest + +import numpy.testing as npt +from stlearn.classes import Spatial + +from .utils import read_test_data + +global adata +adata = read_test_data() + + +class TestSpatial(unittest.TestCase): + """Tests for `stlearn` package.""" + + def test_setup_Spatial(self): + spatial = Spatial(adata) + self.assertIsNotNone(spatial) + self.assertEqual("V1_Breast_Cancer_Block_A_Section_1", spatial.library_id) + self.assertEqual("hires", spatial.img_key) + self.assertEqual(177.4829519178534, spatial.spot_size) + self.assertEqual(True, spatial.crop_coord) + self.assertEqual(False, spatial.use_raw) + npt.assert_array_almost_equal( + [896.782, 1370.627, 1483.498, 1178.713, 1584.901], + spatial.imagecol[:5], decimal=3) + npt.assert_array_almost_equal( + [1549.092, 1158.003, 1040.594, 1373.267, 1021.205], + spatial.imagerow[:5], + decimal=3) From 38829f6a51cb83e4bd32090c5b56866400c6bff3 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 12:27:59 +1000 Subject: [PATCH 051/241] Fixup type checking. --- stlearn/app/source/forms/forms.py | 4 +++- stlearn/plotting/cci_plot_helpers.py | 2 +- stlearn/plotting/classes.py | 18 +++++++++--------- stlearn/plotting/utils.py | 8 ++++---- stlearn/spatials/trajectory/global_level.py | 2 +- stlearn/tools/microenv/cci/analysis.py | 2 +- tests/test_Spatial.py | 8 ++++++-- 7 files changed, 25 insertions(+), 19 deletions(-) diff --git a/stlearn/app/source/forms/forms.py b/stlearn/app/source/forms/forms.py index 790bc97e..466c1da1 100644 --- a/stlearn/app/source/forms/forms.py +++ b/stlearn/app/source/forms/forms.py @@ -217,7 +217,9 @@ def getCCIForm(adata): fields = [] mix = False else: - fields = [key for key in adata.obs.keys() if adata.obs[key].values[0] is str] + fields = [ + key for key in adata.obs.keys() if isinstance(adata.obs[key].values[0], str) + ] mix = fields[0] in adata.uns.keys() element_values = [fields, 20, mix, 0.2, 100] return createSuperForm(elements, element_fields, element_values) diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index 4706b3a2..85efe6ae 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -715,7 +715,7 @@ def chordDiagram(X, ax, colors=None, width=0.1, pad=2, chordwidth=0.7, lim=1.1): ] if len(x) > 10: print("x is too large! Use x smaller than 10") - if colors[0] is str: + if isinstance(colors[0], str): colors = [hex2rgb(colors[i]) for i in range(len(x))] # find position for each start and end diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 6c21f832..862f874e 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -81,7 +81,7 @@ def __init__( self.adata[0].obs[use_label].cat.categories ) else: - if self.list_clusters is not list: + if not isinstance(self.list_clusters, list): self.list_clusters = [self.list_clusters] clusters_indexes = [ @@ -101,12 +101,12 @@ def __init__( stlearn_cmap = ["jana_40", "default"] cmap_available = plt.colormaps() + scanpy_cmap + stlearn_cmap error_msg = ( - "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" + "cmap must be a matplotlib.colors.LinearSegmentedColormap OR " "one of these: " + str(cmap_available) ) - if cmap is str: + if isinstance(cmap, str): assert cmap in cmap_available, error_msg - elif cmap is not matplotlib.colors.LinearSegmentedColormap: + elif not isinstance(cmap, matplotlib.colors.LinearSegmentedColormap): raise Exception(error_msg) self.cmap = cmap @@ -380,7 +380,7 @@ def _plot_genes(self, gene_values: pd.Series): marker="o", vmin=vmin, vmax=vmax, - cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, + cmap=plt.get_cmap(self.cmap) if isinstance(self.cmap, str) else self.cmap, c=gene_values, ) return plot @@ -409,7 +409,7 @@ def _plot_contour(self, gene_values: pd.Series): yi, zi, range(0, int(np.nanmax(zi)) + self.step_size, self.step_size), - cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, + cmap=plt.get_cmap(self.cmap) if isinstance(self.cmap, str) else self.cmap, alpha=self.cell_alpha, ) return cs @@ -547,7 +547,7 @@ def _plot_feature(self, feature_values: pd.Series): marker="o", vmin=vmin, vmax=vmax, - cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, + cmap=plt.get_cmap(self.cmap) if isinstance(self.cmap, str) else self.cmap, c=feature_values, ) return plot @@ -576,7 +576,7 @@ def _plot_contour(self, feature_values: pd.Series): yi, zi, range(0, int(np.nanmax(zi)) + self.step_size, self.step_size), - cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, + cmap=plt.get_cmap(self.cmap) if isinstance(self.cmap, str) else self.cmap, alpha=self.cell_alpha, ) return cs @@ -1031,7 +1031,7 @@ def _plot_subclusters(self, threshold_spots): edgecolor="none", s=self.size, marker="o", - cmap=plt.get_cmap(self.cmap) if self.cmap is str else self.cmap, + cmap=plt.get_cmap(self.cmap) if isinstance(self.cmap, str) else self.cmap, c=colors, alpha=self.cell_alpha, ) diff --git a/stlearn/plotting/utils.py b/stlearn/plotting/utils.py index 6304d740..e819a8be 100644 --- a/stlearn/plotting/utils.py +++ b/stlearn/plotting/utils.py @@ -70,10 +70,10 @@ def get_cmap(cmap): cmap = palettes_st.jana_40 elif cmap == "default": cmap = palettes_st.default - elif cmap is str: # If refers to matplotlib cmap + elif isinstance(cmap, str): # If refers to matplotlib cmap cmap_n = plt.get_cmap(cmap).N return plt.get_cmap(cmap), cmap_n - elif cmap is matplotlib.colors.LinearSegmentedColormap: # already cmap + elif isinstance(cmap, matplotlib.colors.LinearSegmentedColormap): # already cmap cmap_n = cmap.N return cmap, cmap_n @@ -94,9 +94,9 @@ def check_cmap(cmap): "cmap must be a matplotlib.colors.LinearSegmentedColormap OR" "one of these: " + str(cmap_available) ) - if cmap is str: + if isinstance(cmap, str): assert cmap in cmap_available, error_msg - elif cmap is not matplotlib.colors.LinearSegmentedColormap: + elif not isinstance(cmap, matplotlib.colors.LinearSegmentedColormap): raise Exception(error_msg) return cmap diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatials/trajectory/global_level.py index da0cdf35..21d4a3fc 100644 --- a/stlearn/spatials/trajectory/global_level.py +++ b/stlearn/spatials/trajectory/global_level.py @@ -50,7 +50,7 @@ def global_level( inds_cat = {v: k for (k, v) in cat_inds.items()} # Query cluster - if list_clusters[0] is str: + if isinstance(list_clusters[0], str): list_clusters = [cat_inds[label] for label in list_clusters] query_nodes = list_clusters diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index 5318ac08..13c6ae20 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -44,7 +44,7 @@ def load_lrs(names: str | list | None = None, species: str = "human") -> np.ndar """ if names is None: names = ["connectomeDB2020_lit"] - if names is str: + if isinstance(names, str): names = [names] path = os.path.dirname(os.path.realpath(__file__)) diff --git a/tests/test_Spatial.py b/tests/test_Spatial.py index b31fa8cd..7177466a 100644 --- a/tests/test_Spatial.py +++ b/tests/test_Spatial.py @@ -5,6 +5,7 @@ import unittest import numpy.testing as npt + from stlearn.classes import Spatial from .utils import read_test_data @@ -26,8 +27,11 @@ def test_setup_Spatial(self): self.assertEqual(False, spatial.use_raw) npt.assert_array_almost_equal( [896.782, 1370.627, 1483.498, 1178.713, 1584.901], - spatial.imagecol[:5], decimal=3) + spatial.imagecol[:5], + decimal=3, + ) npt.assert_array_almost_equal( [1549.092, 1158.003, 1040.594, 1373.267, 1021.205], spatial.imagerow[:5], - decimal=3) + decimal=3, + ) From 70381ca12057c4ea420c569f2842b35ec158d617 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 13:51:17 +1000 Subject: [PATCH 052/241] Fix warnings and deprecated call of legendHandles. --- stlearn/plotting/classes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 862f874e..18a35aec 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -691,7 +691,8 @@ def _add_cluster_colors(self): # self.adata[0].uns[self.use_label + "_set"] = [] self.adata[0].uns[self.use_label + "_colors"] = [] - for i, cluster in enumerate(self.adata[0].obs.groupby(self.use_label)): + for i, cluster in enumerate(self.adata[0].obs.groupby(self.use_label, + observed=True)): self.adata[0].uns[self.use_label + "_colors"].append( matplotlib.colors.to_hex(self.cmap_(i / (self.cmap_n - 1))) ) @@ -700,7 +701,8 @@ def _add_cluster_colors(self): def _plot_clusters(self): # Plot scatter plot based on pixel of spots - for i, cluster in enumerate(self.query_adata.obs.groupby(self.use_label)): + for i, cluster in enumerate( + self.query_adata.obs.groupby(self.use_label, observed=True)): # Plot scatter plot based on pixel of spots subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), list(cluster[1].index)) @@ -742,7 +744,7 @@ def _add_cluster_bar(self, bbox_to_anchor): handleheight=1.0, edgecolor="white", ) - for handle in lgnd.legendHandles: + for handle in lgnd.legend_handles: handle.set_sizes([20.0]) def _add_cluster_labels(self): From ae7f4bbc8cd1d89bad442872f0e674531f26786d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 13:57:15 +1000 Subject: [PATCH 053/241] Remove deprecated np warnings. --- stlearn/spatials/trajectory/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatials/trajectory/utils.py index 2490b9d7..ce99e9b5 100644 --- a/stlearn/spatials/trajectory/utils.py +++ b/stlearn/spatials/trajectory/utils.py @@ -1,5 +1,6 @@ import networkx as nx import numpy as np +import warnings from numpy import linalg as la from scipy import linalg as spla from scipy import sparse as sps @@ -526,8 +527,8 @@ def _mat_mat_corr_sparse( y_bar = np.reshape(np.mean(Y, axis=0), (1, -1)) y_std = np.reshape(np.std(Y, axis=0), (1, -1)) - with np.warnings.catch_warnings(): - np.warnings.filterwarnings( + with warnings.catch_warnings(): + warnings.filterwarnings( "ignore", r"invalid value encountered in true_divide" ) return (X @ Y - (n * X_bar * y_bar)) / ((n - 1) * X_std * y_std) @@ -602,8 +603,8 @@ def _mat_mat_corr_dense(X: np.ndarray, Y: np.ndarray) -> np.ndarray: y_bar = np.reshape(np_mean(Y, axis=0), (1, -1)) y_std = np.reshape(np_std(Y, axis=0), (1, -1)) - with np.warnings.catch_warnings(): - np.warnings.filterwarnings( + with warnings.catch_warnings(): + warnings.filterwarnings( "ignore", r"invalid value encountered in true_divide" ) return (X @ Y - (n * X_bar * y_bar)) / ((n - 1) * X_std * y_std) From e7c832428ed64860d135e10a38ee7cb43af4d50e Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 16:11:14 +1000 Subject: [PATCH 054/241] Add tests for feature extraction. --- .../image_preprocessing/feature_extractor.py | 102 ++++++++++++------ stlearn/image_preprocessing/image_tiling.py | 65 +++++++---- tests/test_extract_features.py | 62 +++++++++++ tests/test_tiling.py | 82 ++++++++++++++ 4 files changed, 260 insertions(+), 51 deletions(-) create mode 100644 tests/test_extract_features.py create mode 100644 tests/test_tiling.py diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index 50fe976c..b1b35e3d 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -9,17 +9,18 @@ from tqdm import tqdm from .model_zoo import Model, encode +from sklearn.decomposition import PCA _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] - -def extract_feature( +def new_extract_feature( adata: AnnData, cnn_base: _CNN_BASE = "resnet50", n_components: int = 50, + seeds: int = 1, + batch_size: int = 32, verbose: bool = False, copy: bool = False, - seeds: int = 1, ) -> AnnData | None: """\ Extract latent morphological features from H&E images using pre-trained @@ -27,19 +28,21 @@ def extract_feature( Parameters ---------- - adata + adata: Annotated data matrix. - cnn_base + cnn_base: Established convolutional neural network bases choose one from ['resnet50', 'vgg16', 'inception_v3', 'xception'] - n_components + n_components: Number of principal components to compute for latent morphological features - verbose + seeds: + Fix random state + batch_size: + Number of images to process in each batch (default: 32) + verbose: Verbose output - copy + copy: Return a copy instead of writing to adata. - seeds - Fix random state Returns ------- Depending on `copy`, returns or updates `adata` with the following fields. @@ -49,39 +52,78 @@ def extract_feature( adata = adata.copy() if copy else adata - feature_dfs = [] - model = Model(cnn_base) - if "tile_path" not in adata.obs: raise ValueError("Please run the function stlearn.pp.tiling") + model = Model(cnn_base) + n_spots = len(adata) + spots = list(adata.obs["tile_path"].items()) + + spot_names = [] + feature_matrix = None + current_row = 0 + with tqdm( - total=len(adata), + total=n_spots, desc="Extract feature", bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: - for spot, tile_path in adata.obs["tile_path"].items(): - tile = Image.open(tile_path) - tile = np.asarray(tile, dtype="int32") - tile = tile.astype(np.float32) - tile = np.stack([tile]) - if verbose: - print(f"extract feature for spot: {str(spot)}") - features = encode(tile, model) - feature_dfs.append(pd.DataFrame(features, columns=[spot])) - pbar.update(1) - feature_df = pd.concat(feature_dfs, axis=1) + for i in range(0, n_spots, batch_size): + batch_spots = spots[i:i + batch_size] + batch_tiles, batch_spot_names = _load_batch_images(batch_spots, verbose) - adata.obsm["X_tile_feature"] = feature_df.transpose().to_numpy() + if batch_tiles: + batch_array = np.stack(batch_tiles, axis=0) + batch_features = model.predict(batch_array) - from sklearn.decomposition import PCA + if feature_matrix is None: + n_features = batch_features.shape[1] + feature_matrix = np.empty((n_spots, n_features), + dtype=np.float32) - pca = PCA(n_components=n_components, random_state=seeds) - pca.fit(feature_df.transpose().to_numpy()) + end_row = current_row + len(batch_features) + feature_matrix[current_row:end_row] = batch_features + current_row = end_row + + spot_names.extend(batch_spot_names) - adata.obsm["X_morphology"] = pca.transform(feature_df.transpose().to_numpy()) + pbar.update(len(batch_spots)) + + if feature_matrix is None or current_row == 0: + raise ValueError("No features were successfully extracted") + + feature_matrix = feature_matrix[:current_row] + + feature_df = pd.DataFrame(feature_matrix.T, columns=spot_names) + feature_array = feature_df.T.to_numpy() + + adata.obsm["X_tile_feature"] = feature_array + + pca = PCA(n_components=n_components, random_state=seeds) + adata.obsm["X_morphology"] = pca.fit_transform(feature_matrix) print("The morphology feature is added to adata.obsm['X_morphology']!") return adata if copy else None + + +def _load_batch_images(batch_spots, verbose=False): + """Load a batch of images from file paths.""" + images = [] + names = [] + + for spot_name, tile_path in batch_spots: + try: + image = np.asarray(Image.open(tile_path), dtype=np.float32) + images.append(image) + names.append(spot_name) + + if verbose: + print(f"Loaded image for spot: {spot_name}") + + except Exception as e: + print(f"Warning: Failed to load image for spot {spot_name}: {e}") + continue + + return images, names diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index 098cc58a..d782a77e 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -16,6 +16,7 @@ def tiling( crop_size: int = 40, target_size: int = 299, img_fmt: str = "JPEG", + quality: int = 95, verbose: bool = False, copy: bool = False, ) -> AnnData | None: @@ -24,20 +25,24 @@ def tiling( Parameters ---------- - adata + adata: Annotated data matrix. - out_path + out_path: Path to save spot image tiles - library_id + library_id: Library id stored in AnnData. - crop_size + crop_size: Size of tiles - verbose + target_size: + Input size for convolutional neuron network + img_fmt: + Image format ('JPEG' or 'PNG') + quality: + JPEG quality 1-100. + verbose: Verbose output - copy + copy: Return a copy instead of writing to adata. - target_size - Input size for convolutional neuron network Returns ------- Depending on `copy`, returns or updates `adata` with the following fields. @@ -47,23 +52,38 @@ def tiling( adata = adata.copy() if copy else adata + if not isinstance(crop_size, int) or crop_size <= 0: + raise ValueError("crop_size must be a positive integer") + if not isinstance(target_size, int) or target_size <= 0: + raise ValueError("target_size must be a positive integer") + if img_fmt.upper() not in ["JPEG", "PNG"]: + raise ValueError("img_fmt must be 'JPEG' or 'PNG'") + if library_id is None: library_id = list(adata.uns["spatial"].keys())[0] - # Check the exist of out_path - if not os.path.isdir(out_path): - os.mkdir(out_path) + out_path = Path(out_path) + out_path.mkdir(parents=True, exist_ok=True) + + # Load and prepare image + try: + image = adata.uns["spatial"][library_id]["images"][ + adata.uns["spatial"][library_id]["use_quality"] + ] + except KeyError as e: + raise ValueError(f"Could not find image data in adata.uns['spatial']: {e}") - image = adata.uns["spatial"][library_id]["images"][ - adata.uns["spatial"][library_id]["use_quality"] - ] - if image.dtype == np.float32 or image.dtype == np.float64: + if image.dtype in (np.float32, np.float64): + image = np.clip(image, 0, 1) image = (image * 255).astype(np.uint8) + img_pillow = Image.fromarray(image) if img_pillow.mode == "RGBA": img_pillow = img_pillow.convert("RGB") + coordinates = list(zip(adata.obs["imagerow"], adata.obs["imagecol"])) + tile_names = [] with tqdm( @@ -71,14 +91,17 @@ def tiling( desc="Tiling image", bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: - for imagerow, imagecol in zip(adata.obs["imagerow"], adata.obs["imagecol"]): - imagerow_down = imagerow - crop_size / 2 - imagerow_up = imagerow + crop_size / 2 - imagecol_left = imagecol - crop_size / 2 - imagecol_right = imagecol + crop_size / 2 + for imagerow, imagecol in coordinates: + half_crop = crop_size // 2 + imagerow_down = max(0, imagerow - half_crop) + imagerow_up = imagerow + half_crop + imagecol_left = max(0, imagecol - half_crop) + imagecol_right = imagecol + half_crop + tile = img_pillow.crop( (imagecol_left, imagerow_down, imagecol_right, imagerow_up) ) + tile.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) tile = tile.resize((target_size, target_size)) tile_name = str(imagecol) + "-" + str(imagerow) + "-" + str(crop_size) @@ -86,7 +109,7 @@ def tiling( if img_fmt == "JPEG": out_tile = Path(out_path) / (tile_name + ".jpeg") tile_names.append(str(out_tile)) - tile.save(out_tile, "JPEG") + tile.save(out_tile, "JPEG", quality=quality) else: out_tile = Path(out_path) / (tile_name + ".png") tile_names.append(str(out_tile)) diff --git a/tests/test_extract_features.py b/tests/test_extract_features.py new file mode 100644 index 00000000..39f0f694 --- /dev/null +++ b/tests/test_extract_features.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +import unittest +import numpy as np +import tempfile +import shutil +import os + +import scanpy as sc +import stlearn as st + +from .utils import read_test_data + +global adata +adata = read_test_data() + + +class TestFeatureExtractionPerformance(unittest.TestCase): + """Comprehensive tests for feature extraction.""" + + def setUp(self): + """Set up test fixtures.""" + self.test_data = adata.copy() + self.temp_dir = tempfile.mkdtemp() + sc.pp.pca(self.test_data) + st.pp.tiling(self.test_data, self.temp_dir) + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + def test_deterministic_behavior(self): + """Test that results are deterministic with same seed.""" + data1 = self.test_data.copy() + data2 = self.test_data.copy() + + st.pp.extract_feature(data1, seeds=42, batch=16) + st.pp.extract_feature(data2, seeds=42, batch=32) + + np.testing.assert_array_equal( + data1.obsm["X_morphology"], + data2.obsm["X_morphology"], + err_msg="Results should be deterministic with same seed" + ) + + + def test_copy_behavior(self): + """Test copy=True vs copy=False behavior.""" + original_data = self.test_data.copy() + + # Test copy=True + result_copy = st.pp.extract_feature(original_data, copy=True) + self.assertIsNotNone(result_copy) + self.assertNotIn("X_morphology", original_data.obsm) + self.assertIn("X_morphology", result_copy.obsm) + + # Test copy=False + result_inplace = st.pp.extract_feature(original_data, copy=False) + self.assertIsNone(result_inplace) + self.assertIn("X_morphology", original_data.obsm) + diff --git a/tests/test_tiling.py b/tests/test_tiling.py new file mode 100644 index 00000000..90fb2df8 --- /dev/null +++ b/tests/test_tiling.py @@ -0,0 +1,82 @@ + +# !/usr/bin/env python + +"""Tests for tiling function.""" + +import unittest +import time +import numpy as np +import pandas as pd +from pathlib import Path +import tempfile +import shutil +import os +from PIL import Image +from unittest.mock import patch, MagicMock +import filecmp + +import scanpy as sc +import stlearn as st + +from .utils import read_test_data + +global adata +adata = read_test_data() + + +class TestTiling(unittest.TestCase): + """Tests for `stlearn` package.""" + + def setUp(self): + """Set up test fixtures.""" + self.test_data = adata.copy() + self.temp_dir = tempfile.mkdtemp() + self.temp_dir_orig = tempfile.mkdtemp(suffix="_orig") + + # Ensure we have required spatial data + if "spatial" not in self.test_data.uns: + self.skipTest("Test data missing spatial information") + + # Add imagerow/imagecol if missing (for testing) + if "imagerow" not in self.test_data.obs: + # Create synthetic coordinates for testing + n_spots = len(self.test_data) + self.test_data.obs["imagerow"] = np.random.randint(50, 450, n_spots) + self.test_data.obs["imagecol"] = np.random.randint(50, 450, n_spots) + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + if os.path.exists(self.temp_dir_orig): + shutil.rmtree(self.temp_dir_orig) + + def test_directory_creation(self): + """Test directory creation behavior.""" + + # Test nested directory creation + nested_path = Path(self.temp_dir) / "level1" / "level2" / "tiles" + data = self.test_data.copy() + + st.pp.tiling(data, nested_path) + + self.assertTrue(nested_path.exists(), "Nested directories not created") + self.assertGreater(len(list(nested_path.glob("*"))), 0, + "No files in nested directory") + + def test_quality_parameter(self): + """Test JPEG quality parameter.""" + data = self.test_data[:3].copy() # Small subset + + # Test different quality settings + for quality in [50, 95]: + temp_quality = tempfile.mkdtemp(suffix=f"_q{quality}") + test_data = data.copy() + + st.pp.tiling(test_data, temp_quality, img_fmt="JPEG", quality=quality) + + # Verify files exist + jpeg_files = list(Path(temp_quality).glob("*.jpeg")) + self.assertEqual(len(jpeg_files), len(data)) + + shutil.rmtree(temp_quality) \ No newline at end of file From 9e5f8b825d16d31c47c9a729fbc0fb670d1f42c4 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 16:13:01 +1000 Subject: [PATCH 055/241] Oops. --- stlearn/image_preprocessing/feature_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index b1b35e3d..0fcb9b87 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -13,7 +13,7 @@ _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] -def new_extract_feature( +def extract_feature( adata: AnnData, cnn_base: _CNN_BASE = "resnet50", n_components: int = 50, From 242a7deaa10b83951a0859c2834d48b4a1dafcb5 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 17:17:24 +1000 Subject: [PATCH 056/241] Oops. --- .../image_preprocessing/feature_extractor.py | 75 ++++++++++++++++++- stlearn/pp.py | 2 +- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index 0fcb9b87..e7dc8a0e 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -13,7 +13,7 @@ _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] -def extract_feature( +def new_extract_feature( adata: AnnData, cnn_base: _CNN_BASE = "resnet50", n_components: int = 50, @@ -127,3 +127,76 @@ def _load_batch_images(batch_spots, verbose=False): continue return images, names + +def extract_feature( + adata: AnnData, + cnn_base: _CNN_BASE = "resnet50", + n_components: int = 50, + verbose: bool = False, + copy: bool = False, + seeds: int = 1, +) -> AnnData | None: + """\ + Extract latent morphological features from H&E images using pre-trained + convolutional neural network base + + Parameters + ---------- + adata + Annotated data matrix. + cnn_base + Established convolutional neural network bases + choose one from ['resnet50', 'vgg16', 'inception_v3', 'xception'] + n_components + Number of principal components to compute for latent morphological features + verbose + Verbose output + copy + Return a copy instead of writing to adata. + seeds + Fix random state + Returns + ------- + Depending on `copy`, returns or updates `adata` with the following fields. + **X_morphology** : `adata.obsm` field + Dimension reduced latent morphological features. + """ + + adata = adata.copy() if copy else adata + + feature_dfs = [] + model = Model(cnn_base) + + if "tile_path" not in adata.obs: + raise ValueError("Please run the function stlearn.pp.tiling") + + with tqdm( + total=len(adata), + desc="Extract feature", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", + ) as pbar: + for spot, tile_path in adata.obs["tile_path"].items(): + tile = Image.open(tile_path) + tile = np.asarray(tile, dtype="int32") + tile = tile.astype(np.float32) + tile = np.stack([tile]) + if verbose: + print(f"extract feature for spot: {str(spot)}") + features = encode(tile, model) + feature_dfs.append(pd.DataFrame(features, columns=[spot])) + pbar.update(1) + + feature_df = pd.concat(feature_dfs, axis=1) + + adata.obsm["X_tile_feature"] = feature_df.transpose().to_numpy() + + from sklearn.decomposition import PCA + + pca = PCA(n_components=n_components, random_state=seeds) + pca.fit(feature_df.transpose().to_numpy()) + + adata.obsm["X_morphology"] = pca.transform(feature_df.transpose().to_numpy()) + + print("The morphology feature is added to adata.obsm['X_morphology']!") + + return adata if copy else None diff --git a/stlearn/pp.py b/stlearn/pp.py index 695efd24..931e1c17 100644 --- a/stlearn/pp.py +++ b/stlearn/pp.py @@ -1,4 +1,4 @@ -from .image_preprocessing.feature_extractor import extract_feature +from .image_preprocessing.feature_extractor import extract_feature, new_extract_feature from .image_preprocessing.image_tiling import tiling from .preprocessing.filter_genes import filter_genes from .preprocessing.graph import neighbors From 3d96012d1305b4c7ee6a38e5ed4a4b2e38cc74d0 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 19:05:53 +1000 Subject: [PATCH 057/241] Refactor. --- stlearn/image_preprocessing/feature_extractor.py | 10 +++++++++- stlearn/image_preprocessing/model_zoo.py | 6 ------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index e7dc8a0e..997ee448 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -77,6 +77,8 @@ def new_extract_feature( batch_array = np.stack(batch_tiles, axis=0) batch_features = model.predict(batch_array) + batch_features = batch_features.reshape(batch_features.shape[0], -1) + if feature_matrix is None: n_features = batch_features.shape[1] feature_matrix = np.empty((n_spots, n_features), @@ -128,6 +130,12 @@ def _load_batch_images(batch_spots, verbose=False): return images, names + +def _encode(tiles, model): + features = model.predict(tiles) + features = features.ravel() + return features + def extract_feature( adata: AnnData, cnn_base: _CNN_BASE = "resnet50", @@ -182,7 +190,7 @@ def extract_feature( tile = np.stack([tile]) if verbose: print(f"extract feature for spot: {str(spot)}") - features = encode(tile, model) + features = _encode(tile, model) feature_dfs.append(pd.DataFrame(features, columns=[spot])) pbar.update(1) diff --git a/stlearn/image_preprocessing/model_zoo.py b/stlearn/image_preprocessing/model_zoo.py index c21af134..1969f9b1 100644 --- a/stlearn/image_preprocessing/model_zoo.py +++ b/stlearn/image_preprocessing/model_zoo.py @@ -1,9 +1,3 @@ -def encode(tiles, model): - features = model.predict(tiles) - features = features.ravel() - return features - - class Model: __name__ = "CNN base model" From 3b04b30ea35b7452a3cbb5e5003c5e9fe433d6bc Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 19:08:58 +1000 Subject: [PATCH 058/241] Fix. --- stlearn/image_preprocessing/feature_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index 997ee448..c7d3c8a1 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -8,7 +8,7 @@ # Test progress bar from tqdm import tqdm -from .model_zoo import Model, encode +from .model_zoo import Model from sklearn.decomposition import PCA _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] From 86a8232fddb1c4708318a36ca8775fc969c264e8 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 20:39:11 +1000 Subject: [PATCH 059/241] Fix. --- .../image_preprocessing/feature_extractor.py | 200 ++++-------------- stlearn/pp.py | 2 +- tests/test_extract_features.py | 9 +- 3 files changed, 55 insertions(+), 156 deletions(-) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index c7d3c8a1..0fe7d5fc 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -1,26 +1,23 @@ from typing import Literal import numpy as np -import pandas as pd -from anndata import AnnData from PIL import Image - -# Test progress bar +from anndata import AnnData +from sklearn.decomposition import PCA from tqdm import tqdm from .model_zoo import Model -from sklearn.decomposition import PCA _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] -def new_extract_feature( - adata: AnnData, - cnn_base: _CNN_BASE = "resnet50", - n_components: int = 50, - seeds: int = 1, - batch_size: int = 32, - verbose: bool = False, - copy: bool = False, + +def extract_feature( + adata: AnnData, + cnn_base: _CNN_BASE = "resnet50", + n_components: int = 50, + seeds: int = 1, + verbose: bool = False, + copy: bool = False, ) -> AnnData | None: """\ Extract latent morphological features from H&E images using pre-trained @@ -37,8 +34,6 @@ def new_extract_feature( Number of principal components to compute for latent morphological features seeds: Fix random state - batch_size: - Number of images to process in each batch (default: 32) verbose: Verbose output copy: @@ -48,6 +43,10 @@ def new_extract_feature( Depending on `copy`, returns or updates `adata` with the following fields. **X_morphology** : `adata.obsm` field Dimension reduced latent morphological features. + Raises + ------ + ValueError + If any image fails to process or if tile_path column is missing. """ adata = adata.copy() if copy else adata @@ -56,155 +55,50 @@ def new_extract_feature( raise ValueError("Please run the function stlearn.pp.tiling") model = Model(cnn_base) - n_spots = len(adata) - spots = list(adata.obs["tile_path"].items()) - - spot_names = [] - feature_matrix = None - current_row = 0 - - with tqdm( - total=n_spots, - desc="Extract feature", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", - ) as pbar: - - for i in range(0, n_spots, batch_size): - batch_spots = spots[i:i + batch_size] - batch_tiles, batch_spot_names = _load_batch_images(batch_spots, verbose) - - if batch_tiles: - batch_array = np.stack(batch_tiles, axis=0) - batch_features = model.predict(batch_array) - - batch_features = batch_features.reshape(batch_features.shape[0], -1) - - if feature_matrix is None: - n_features = batch_features.shape[1] - feature_matrix = np.empty((n_spots, n_features), - dtype=np.float32) - - end_row = current_row + len(batch_features) - feature_matrix[current_row:end_row] = batch_features - current_row = end_row - - spot_names.extend(batch_spot_names) - - pbar.update(len(batch_spots)) - - if feature_matrix is None or current_row == 0: - raise ValueError("No features were successfully extracted") - - feature_matrix = feature_matrix[:current_row] - - feature_df = pd.DataFrame(feature_matrix.T, columns=spot_names) - feature_array = feature_df.T.to_numpy() - - adata.obsm["X_tile_feature"] = feature_array - - pca = PCA(n_components=n_components, random_state=seeds) - adata.obsm["X_morphology"] = pca.fit_transform(feature_matrix) - - print("The morphology feature is added to adata.obsm['X_morphology']!") - - return adata if copy else None - - -def _load_batch_images(batch_spots, verbose=False): - """Load a batch of images from file paths.""" - images = [] - names = [] - - for spot_name, tile_path in batch_spots: - try: - image = np.asarray(Image.open(tile_path), dtype=np.float32) - images.append(image) - names.append(spot_name) - - if verbose: - print(f"Loaded image for spot: {spot_name}") - - except Exception as e: - print(f"Warning: Failed to load image for spot {spot_name}: {e}") - continue - - return images, names + # Pre-allocate feature matrix, spot names and arrays to avoid overhead + tile_paths = adata.obs["tile_path"].values + n_spots = len(tile_paths) + if n_spots == 0: + raise ValueError("No tile paths found in adata.obs['tile_path']") -def _encode(tiles, model): - features = model.predict(tiles) - features = features.ravel() - return features + first_features = _read_and_predict(tile_paths[0], model, verbose=verbose) + n_features = len(first_features) -def extract_feature( - adata: AnnData, - cnn_base: _CNN_BASE = "resnet50", - n_components: int = 50, - verbose: bool = False, - copy: bool = False, - seeds: int = 1, -) -> AnnData | None: - """\ - Extract latent morphological features from H&E images using pre-trained - convolutional neural network base - - Parameters - ---------- - adata - Annotated data matrix. - cnn_base - Established convolutional neural network bases - choose one from ['resnet50', 'vgg16', 'inception_v3', 'xception'] - n_components - Number of principal components to compute for latent morphological features - verbose - Verbose output - copy - Return a copy instead of writing to adata. - seeds - Fix random state - Returns - ------- - Depending on `copy`, returns or updates `adata` with the following fields. - **X_morphology** : `adata.obsm` field - Dimension reduced latent morphological features. - """ - - adata = adata.copy() if copy else adata - - feature_dfs = [] - model = Model(cnn_base) - - if "tile_path" not in adata.obs: - raise ValueError("Please run the function stlearn.pp.tiling") + # Setup feature matrix + feature_matrix = np.empty((n_spots, n_features), dtype=np.float32) + feature_matrix[0] = first_features with tqdm( - total=len(adata), - desc="Extract feature", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", + total=n_spots, + desc="Extract feature", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", + initial=1, # We already processed the first image ) as pbar: - for spot, tile_path in adata.obs["tile_path"].items(): - tile = Image.open(tile_path) - tile = np.asarray(tile, dtype="int32") - tile = tile.astype(np.float32) - tile = np.stack([tile]) - if verbose: - print(f"extract feature for spot: {str(spot)}") - features = _encode(tile, model) - feature_dfs.append(pd.DataFrame(features, columns=[spot])) + for i in range(1, n_spots): + features = _read_and_predict(tile_paths[i], model, verbose=verbose) + feature_matrix[i] = features pbar.update(1) - feature_df = pd.concat(feature_dfs, axis=1) + adata.obsm["X_tile_feature"] = feature_matrix + pca = PCA(n_components=n_components, random_state=seeds) + pca.fit(feature_matrix) + adata.obsm["X_morphology"] = pca.transform(feature_matrix) - adata.obsm["X_tile_feature"] = feature_df.transpose().to_numpy() + print("The morphology feature is added to adata.obsm['X_morphology']!") - from sklearn.decomposition import PCA + return adata if copy else None - pca = PCA(n_components=n_components, random_state=seeds) - pca.fit(feature_df.transpose().to_numpy()) - adata.obsm["X_morphology"] = pca.transform(feature_df.transpose().to_numpy()) +def _read_and_predict(path, model, verbose=False): + try: + with Image.open(path) as img: + tile = np.asarray(img, dtype=np.float32) - print("The morphology feature is added to adata.obsm['X_morphology']!") + if verbose: + print(f"Loaded image: {path}") - return adata if copy else None + tile = tile[np.newaxis, ...] + return model.predict(tile).ravel() + except Exception as e: + raise ValueError(f"Failed to process image: {path}. Error: {str(e)}") diff --git a/stlearn/pp.py b/stlearn/pp.py index 931e1c17..695efd24 100644 --- a/stlearn/pp.py +++ b/stlearn/pp.py @@ -1,4 +1,4 @@ -from .image_preprocessing.feature_extractor import extract_feature, new_extract_feature +from .image_preprocessing.feature_extractor import extract_feature from .image_preprocessing.image_tiling import tiling from .preprocessing.filter_genes import filter_genes from .preprocessing.graph import neighbors diff --git a/tests/test_extract_features.py b/tests/test_extract_features.py index 39f0f694..6a6651f3 100644 --- a/tests/test_extract_features.py +++ b/tests/test_extract_features.py @@ -35,14 +35,19 @@ def test_deterministic_behavior(self): data1 = self.test_data.copy() data2 = self.test_data.copy() - st.pp.extract_feature(data1, seeds=42, batch=16) - st.pp.extract_feature(data2, seeds=42, batch=32) + st.pp.extract_feature(data1, seeds=42) + st.pp.extract_feature(data2, seeds=42) np.testing.assert_array_equal( data1.obsm["X_morphology"], data2.obsm["X_morphology"], err_msg="Results should be deterministic with same seed" ) + np.testing.assert_array_equal( + data1.obsm["X_tile_feature"], + data2.obsm["X_tile_feature"], + err_msg="Results should be deterministic with same seed" + ) def test_copy_behavior(self): From 7a7f9ed32c179766dd4c7c4fe0f808f72f90cbf9 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 20:47:27 +1000 Subject: [PATCH 060/241] Fix. --- stlearn/image_preprocessing/feature_extractor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index 0fe7d5fc..e49c21ed 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -78,7 +78,8 @@ def extract_feature( for i in range(1, n_spots): features = _read_and_predict(tile_paths[i], model, verbose=verbose) feature_matrix[i] = features - pbar.update(1) + if i % 10 == 0: + pbar.update(10) adata.obsm["X_tile_feature"] = feature_matrix pca = PCA(n_components=n_components, random_state=seeds) From a159053784b85a0953c2932d7ece970b5e7bb110 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 20:47:38 +1000 Subject: [PATCH 061/241] Fix. --- stlearn/image_preprocessing/feature_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index e49c21ed..bc1f52f5 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -79,7 +79,7 @@ def extract_feature( features = _read_and_predict(tile_paths[i], model, verbose=verbose) feature_matrix[i] = features if i % 10 == 0: - pbar.update(10) + pbar.update(100) adata.obsm["X_tile_feature"] = feature_matrix pca = PCA(n_components=n_components, random_state=seeds) From e3e9bfc075ef1d121a126bef73a9e6d81a101290 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 10 Jun 2025 20:52:29 +1000 Subject: [PATCH 062/241] Fix. --- stlearn/image_preprocessing/feature_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index bc1f52f5..dd8fee92 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -78,7 +78,7 @@ def extract_feature( for i in range(1, n_spots): features = _read_and_predict(tile_paths[i], model, verbose=verbose) feature_matrix[i] = features - if i % 10 == 0: + if i % 100 == 0: pbar.update(100) adata.obsm["X_tile_feature"] = feature_matrix From c9b0e4265a869e5757d633608c8b06e046afc900 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 11 Jun 2025 14:38:14 +1000 Subject: [PATCH 063/241] Don't have plot fail if already called before. --- stlearn/plotting/classes.py | 20 +++--- tests/test_cluster_plot.py | 127 ++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 tests/test_cluster_plot.py diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 18a35aec..41450bae 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -687,29 +687,25 @@ def __init__( self._save_output() def _add_cluster_colors(self): - if self.use_label + "_colors" not in self.adata[0].uns: - # self.adata[0].uns[self.use_label + "_set"] = [] - self.adata[0].uns[self.use_label + "_colors"] = [] - - for i, cluster in enumerate(self.adata[0].obs.groupby(self.use_label, - observed=True)): - self.adata[0].uns[self.use_label + "_colors"].append( - matplotlib.colors.to_hex(self.cmap_(i / (self.cmap_n - 1))) - ) - # self.adata[0].uns[self.use_label + "_set"].append( cluster[0] ) + self.adata[0].uns[self.use_label + "_colors"] = [] + + for i, cluster in enumerate(self.adata[0].obs.groupby(self.use_label, + observed=True)): + self.adata[0].uns[self.use_label + "_colors"].append( + matplotlib.colors.to_hex(self.cmap_(i / (self.cmap_n - 1))) + ) def _plot_clusters(self): # Plot scatter plot based on pixel of spots for i, cluster in enumerate( - self.query_adata.obs.groupby(self.use_label, observed=True)): + self.query_adata.obs.groupby(self.use_label, observed=True)): # Plot scatter plot based on pixel of spots subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), list(cluster[1].index)) ] if self.use_label + "_colors" in self.adata[0].uns: - # label_set = self.adata[0].uns[self.use_label+'_set'] label_set = ( self.adata[0].obs[self.use_label].cat.categories.values.astype(str) ) diff --git a/tests/test_cluster_plot.py b/tests/test_cluster_plot.py new file mode 100644 index 00000000..4f04afcd --- /dev/null +++ b/tests/test_cluster_plot.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +"""Tests for ClusterPlot.""" + +import unittest +from unittest.mock import patch, MagicMock +import numpy as np +import pandas as pd +import matplotlib.colors +import matplotlib.pyplot as plt + +from stlearn.plotting.classes import ClusterPlot +from .utils import read_test_data + +global adata +adata = read_test_data() + + +class TestClusterPlot(unittest.TestCase): + """Tests for ClusterPlot.""" + + def setUp(self): + """Set up test data with known clusters.""" + self.adata = adata.copy() + + # Create test clustering data + n_spots = len(self.adata.obs) + cluster_labels = np.random.choice(['Cluster_0', 'Cluster_1', 'Cluster_2'], + n_spots) + self.adata.obs['test_clusters'] = pd.Categorical(cluster_labels) + + # Ensure we have a clean slate + if 'test_clusters_colors' in self.adata.uns: + del self.adata.uns['test_clusters_colors'] + + def test_color_generation_first_call(self): + """Test that colors are generated correctly on first call.""" + with patch('matplotlib.pyplot.subplots') as mock_subplots, \ + patch.object(ClusterPlot, '_plot_clusters') as mock_plot, \ + patch.object(ClusterPlot, '_add_image'): + # Mock matplotlib components + mock_fig, mock_ax = MagicMock(), MagicMock() + mock_subplots.return_value = (mock_fig, mock_ax) + + # Create ClusterPlot + label_name = 'test_clusters' + plot = ClusterPlot( + adata=self.adata, + use_label=label_name, + show_image=False, + show_color_bar=False + ) + + # Check that colors were generated + colors = plot.adata[0].uns[f"{label_name}_colors"] + self.assertIsNotNone(colors) + self.assertEqual(len(colors), 3) # 3 clusters + + # Check that all colors are valid hex colors + for color in colors: + self.assertTrue(matplotlib.colors.is_color_like(color)) + self.assertTrue(color.startswith('#')) + self.assertEqual(len(color), 7) # #RRGGBB format + + def test_multiple_calls_same_adata(self): + """Test that multiple calls with same adata work correctly.""" + with patch('matplotlib.pyplot.subplots') as mock_subplots, \ + patch.object(ClusterPlot, '_plot_clusters') as mock_plot, \ + patch.object(ClusterPlot, '_add_image'): + mock_fig, mock_ax = MagicMock(), MagicMock() + mock_subplots.return_value = (mock_fig, mock_ax) + + label_name = 'test_clusters' + + # First call + plot1 = ClusterPlot( + adata=self.adata, + use_label=label_name, + show_image=False, + show_color_bar=False + ) + + # Second call with same adata + plot2 = ClusterPlot( + adata=self.adata, + use_label=label_name, + show_image=False, + show_color_bar=False + ) + + # Both should succeed and generate consistent colors + colors1 = plot1.adata[0].uns[f"{label_name}_colors"] + colors2 = plot2.adata[0].uns[f"{label_name}_colors"] + + self.assertEqual(len(colors1), len(colors2)) + self.assertEqual(colors1, colors2) + + def test_insufficient_existing_colors_extended(self): + """Test that insufficient existing colors are extended.""" + # Pre-populate adata with insufficient colors (only 2 colors for 3 clusters) + existing_colors = ['#FF0000', '#00FF00'] + label_name = 'test_clusters' + self.adata.uns[f'{label_name}_colors'] = existing_colors + + with patch('matplotlib.pyplot.subplots') as mock_subplots, \ + patch.object(ClusterPlot, '_plot_clusters') as mock_plot, \ + patch.object(ClusterPlot, '_add_image'): + mock_fig, mock_ax = MagicMock(), MagicMock() + mock_subplots.return_value = (mock_fig, mock_ax) + + plot = ClusterPlot( + adata=self.adata, + use_label=label_name, + show_image=False, + show_color_bar=False + ) + + # Should extend existing colors + colors = plot.adata[0].uns[f"{label_name}_colors"] + self.assertEqual(len(colors), 3) + self.assertNotEqual(colors[:2], existing_colors) + + def tearDown(self): + """Clean up after each test.""" + # Clear any test artifacts + if hasattr(self, 'adata') and 'test_clusters_colors' in self.adata.uns: + del self.adata.uns['test_clusters_colors'] From 4200d2bb744bb8e4b0d9e10aac43d6147c5589d8 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 11 Jun 2025 16:28:59 +1000 Subject: [PATCH 064/241] Fix up copy semantics. --- stlearn/preprocessing/graph.py | 10 ++++------ stlearn/tools/clustering/louvain.py | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/stlearn/preprocessing/graph.py b/stlearn/preprocessing/graph.py index ccc381ed..3f69bfee 100644 --- a/stlearn/preprocessing/graph.py +++ b/stlearn/preprocessing/graph.py @@ -7,7 +7,7 @@ from anndata import AnnData from numpy.random import RandomState -_Method = Literal["umap", "gauss", "rapids"] +_Method = Literal["umap", "gauss"] _MetricFn = Callable[[np.ndarray, np.ndarray], float] # from sklearn.metrics.pairwise_distances.__doc__: _MetricSparseCapable = Literal[ @@ -42,7 +42,7 @@ def neighbors( use_rep: str | None = None, knn: bool = True, random_state: int | RandomState | None = 0, - method: _Method | None = "umap", + method: _Method = "umap", metric: _Metric | _MetricFn = "euclidean", metric_kwds: Mapping[str, Any] = MappingProxyType({}), copy: bool = False, @@ -78,8 +78,6 @@ def neighbors( method Use 'umap' [McInnes18]_ or 'gauss' (Gauss kernel following [Coifman05]_ with adaptive width [Haghverdi16]_) for computing connectivities. - Use 'rapids' for the RAPIDS implementation of UMAP (experimental, GPU - only). metric A known metric’s name or a callable that returns a distance. metric_kwds @@ -97,7 +95,7 @@ def neighbors( neighbors. """ - scanpy.pp.neighbors( + adata = scanpy.pp.neighbors( adata, n_neighbors=n_neighbors, n_pcs=n_pcs, @@ -112,4 +110,4 @@ def neighbors( print("Created k-Nearest-Neighbor graph in adata.uns['neighbors'] ") - return adata + return adata if copy else None diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index e0426662..18bfe9e6 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -34,37 +34,37 @@ def louvain( or explicitly passing a ``adjacency`` matrix. Parameters ---------- - adata + adata: The annotated data matrix. - resolution + resolution: For the default flavor (``'vtraag'``), you can provide a resolution (higher resolution means finding more and smaller clusters), which defaults to 1.0. See “Time as a resolution parameter” in [Lambiotte09]_. - random_state + random_state: Change the initialization of the optimization. - restrict_to + restrict_to: Restrict the cluster to the categories within the key for sample annotation, tuple needs to contain ``(obs_key, list_of_categories)``. - key_added + key_added: Key under which to add the cluster labels. (default: ``'louvain'``) - adjacency + adjacency: Sparse adjacency matrix of the graph, defaults to ``adata.uns['neighbors']['connectivities']``. - flavor + flavor: Choose between to packages for computing the cluster. ``'vtraag'`` is much more powerful, and the default. - directed + directed: Interpret the ``adjacency`` matrix as directed graph? - use_weights + use_weights: Use weights from knn graph. - partition_type + partition_type: Type of partition to use. Only a valid argument if ``flavor`` is ``'vtraag'``. - partition_kwargs + partition_kwargs: Key word arguments to pass to partitioning, if ``vtraag`` method is being used. - copy + copy: Copy adata or modify it inplace. Returns ------- @@ -77,7 +77,7 @@ def louvain( When ``copy=True`` is set, a copy of ``adata`` with those fields is returned. """ - scanpy.tl.louvain( + adata = scanpy.tl.louvain( adata, resolution=resolution, random_state=random_state, @@ -97,4 +97,4 @@ def louvain( "Louvain cluster is done! The labels are stored in adata.obs['%s']" % key_added ) - return adata + return adata if copy else None \ No newline at end of file From 21f7e94f6a1d2b5470c753dd399fea00afa291d8 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 08:09:49 +1000 Subject: [PATCH 065/241] Only support 3.10 for now. --- pyproject.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 84795255..4ca3d373 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ description = "A downstream analysis toolkit for Spatial Transcriptomic data" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "BSD license"} -requires-python = ">=3.10" +requires-python = "==3.10" keywords = ["stlearn"] classifiers = [ "Development Status :: 2 - Pre-Alpha", @@ -19,9 +19,6 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", ] dynamic = ["dependencies"] From 1f5e329d45fa4f4af98ae9ee7041996678013947 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 08:12:16 +1000 Subject: [PATCH 066/241] Only support 3.10 for now. --- setup.py | 55 ------------------------------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 292288be..00000000 --- a/setup.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python - -"""The setup script.""" - -from setuptools import setup, find_packages - -with open("README.md", encoding="utf8") as readme_file: - readme = readme_file.read() - -with open("HISTORY.rst") as history_file: - history = history_file.read() - -with open("requirements.txt") as f: - requirements = f.read().splitlines() - - -setup_requirements = [] - -test_requirements = [] - -setup( - author="Genomics and Machine Learning lab", - author_email="andrew.newman@uq.edu.au", - python_requires=">=3.10", - classifiers=[ - "Development Status :: 2 - Pre-Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - ], - description="A downstream analysis toolkit for Spatial Transcriptomic data", - entry_points={ - "console_scripts": [ - "stlearn=stlearn.app.cli:main", - ], - }, - install_requires=requirements, - license="BSD license", - long_description=readme + "\n\n" + history, - long_description_content_type="text/markdown", - include_package_data=True, - keywords="stlearn", - name="stlearn", - packages=find_packages(include=["stlearn", "stlearn.*"]), - setup_requires=setup_requirements, - test_suite="tests", - tests_require=test_requirements, - url="https://github.com/BiomedicalMachineLearning/stLearn", - version="0.4.2", - zip_safe=False, -) From db8b19d3a6abd14d278ad95404b3c3d9dff4aa08 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 08:20:14 +1000 Subject: [PATCH 067/241] Make similar to previous code. --- stlearn/adds/parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/adds/parsing.py b/stlearn/adds/parsing.py index 0ae6a9f0..88847fbd 100644 --- a/stlearn/adds/parsing.py +++ b/stlearn/adds/parsing.py @@ -29,7 +29,7 @@ def parsing( # Get a map of the new coordinates new_coordinates = dict() - with open(coordinates_file) as filehandler: + with open(coordinates_file, mode='r') as filehandler: for line in filehandler.readlines(): tokens = line.split() assert len(tokens) >= 6 or len(tokens) == 4 From 7885722c507bea89bb6b8c1b4b9851cc393bcc1f Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 12:57:01 +1000 Subject: [PATCH 068/241] Make similar to previous code. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ca3d373..3b599795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ description = "A downstream analysis toolkit for Spatial Transcriptomic data" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "BSD license"} -requires-python = "==3.10" +requires-python = "~=3.10.0" keywords = ["stlearn"] classifiers = [ "Development Status :: 2 - Pre-Alpha", From e1d9db52c4232157539c3e419888a2853bb8800c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 13:09:32 +1000 Subject: [PATCH 069/241] Don't need to check if copy is on - just return. --- stlearn/tools/clustering/louvain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index 18bfe9e6..af28430f 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -97,4 +97,4 @@ def louvain( "Louvain cluster is done! The labels are stored in adata.obs['%s']" % key_added ) - return adata if copy else None \ No newline at end of file + return adata \ No newline at end of file From e82fd288d4b77baf8cc9a1a8fd8a51b6c08bb4ea Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 13:30:03 +1000 Subject: [PATCH 070/241] Small fixes to types and name. --- stlearn/preprocessing/log_scale.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/stlearn/preprocessing/log_scale.py b/stlearn/preprocessing/log_scale.py index 2faf99cf..4a434507 100644 --- a/stlearn/preprocessing/log_scale.py +++ b/stlearn/preprocessing/log_scale.py @@ -46,11 +46,11 @@ def log1p( def scale( - adata: AnnData | np.ndarray | spmatrix, + data: AnnData | spmatrix | np.ndarray, zero_center: bool = True, max_value: float | None = None, copy: bool = False, -) -> AnnData | None: +) -> AnnData | spmatrix | np.ndarray | None: """\ Wrap function of scanpy.pp.scale @@ -61,7 +61,7 @@ def scale( the future, they might be set to NaNs. Parameters ---------- - data + data: The (annotated) data matrix of shape `n_obs` × `n_vars`. Rows correspond to cells and columns to genes. zero_center @@ -74,11 +74,11 @@ def scale( determines whether a copy is returned. Returns ------- - Depending on `copy` returns or updates `adata` with a scaled `adata.X`. + Depending on `copy` returns or updates `data` with a scaled `data.X`. """ result = scanpy.pp.scale( - adata, zero_center=zero_center, max_value=max_value, copy=copy + data, zero_center=zero_center, max_value=max_value, copy=copy ) print("Scale step is finished in adata.X") return result From fda0ca53281c8364e2e0137265c75df8dd13e16b Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 13:37:34 +1000 Subject: [PATCH 071/241] Add old one back again. --- .../image_preprocessing/feature_extractor.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index dd8fee92..6f521ec8 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -5,12 +5,89 @@ from anndata import AnnData from sklearn.decomposition import PCA from tqdm import tqdm +import pandas as pd from .model_zoo import Model _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] +def old_extract_feature( + adata: AnnData, + cnn_base: _CNN_BASE = "resnet50", + n_components: int = 50, + verbose: bool = False, + copy: bool = False, + seeds: int = 1, +) -> AnnData | None: + """\ + Extract latent morphological features from H&E images using pre-trained + convolutional neural network base + + Parameters + ---------- + adata: + Annotated data matrix. + cnn_base: + Established convolutional neural network bases + choose one from ['resnet50', 'vgg16', 'inception_v3', 'xception'] + n_components: + Number of principal components to compute for latent morphological features + verbose: + Verbose output + copy: + Return a copy instead of writing to adata. + seeds: + Fix random state + Returns + ------- + Depending on `copy`, returns or updates `adata` with the following fields. + **X_morphology** : `adata.obsm` field + Dimension reduced latent morphological features. + """ + feature_dfs = [] + model = Model(cnn_base) + + if "tile_path" not in adata.obs: + raise ValueError("Please run the function stlearn.pp.tiling") + + def encode(tiles, model): + features = model.predict(tiles) + features = features.ravel() + return features + + with tqdm( + total=len(adata), + desc="Extract feature", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", + ) as pbar: + for spot, tile_path in adata.obs["tile_path"].items(): + tile = Image.open(tile_path) + tile = np.asarray(tile, dtype="int32") + tile = tile.astype(np.float32) + tile = np.stack([tile]) + if verbose: + print("extract feature for spot: {}".format(str(spot))) + features = encode(tile, model) + feature_dfs.append(pd.DataFrame(features, columns=[spot])) + pbar.update(1) + + feature_df = pd.concat(feature_dfs, axis=1) + + adata.obsm["X_tile_feature"] = feature_df.transpose().to_numpy() + + from sklearn.decomposition import PCA + + pca = PCA(n_components=n_components, random_state=seeds) + pca.fit(feature_df.transpose().to_numpy()) + + adata.obsm["X_morphology"] = pca.transform(feature_df.transpose().to_numpy()) + + print("The morphology feature is added to adata.obsm['X_morphology']!") + + return adata if copy else None + + def extract_feature( adata: AnnData, cnn_base: _CNN_BASE = "resnet50", From 9c9e1e801b3410c8be11d5f33b5f9690d28233d5 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 13:41:52 +1000 Subject: [PATCH 072/241] Add old one back again. --- stlearn/pp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stlearn/pp.py b/stlearn/pp.py index 695efd24..b91eb648 100644 --- a/stlearn/pp.py +++ b/stlearn/pp.py @@ -1,4 +1,4 @@ -from .image_preprocessing.feature_extractor import extract_feature +from .image_preprocessing.feature_extractor import extract_feature, old_extract_feature from .image_preprocessing.image_tiling import tiling from .preprocessing.filter_genes import filter_genes from .preprocessing.graph import neighbors @@ -13,4 +13,5 @@ "neighbors", "tiling", "extract_feature", + "old_extract_feature", ] From 63b4d746ca30c0bcd36fca4b7db27fdb3d3c97ba Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 14:13:11 +1000 Subject: [PATCH 073/241] Add old tiling back. --- stlearn/image_preprocessing/image_tiling.py | 104 ++++++++++++++++++-- stlearn/pp.py | 3 +- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index d782a77e..f1ffba94 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -1,12 +1,104 @@ -import os -from pathlib import Path - -import numpy as np +from typing import Optional, Union from anndata import AnnData from PIL import Image - -# Test progress bar +from pathlib import Path from tqdm import tqdm +import numpy as np +import os + + +def old_tiling( + adata: AnnData, + out_path: Union[Path, str] = "./tiling", + library_id: Union[str, None] = None, + crop_size: int = 40, + target_size: int = 299, + img_fmt: str = "JPEG", + verbose: bool = False, + copy: bool = False, +) -> Optional[AnnData]: + """\ + Tiling H&E images to small tiles based on spot spatial location + + Parameters + ---------- + adata + Annotated data matrix. + out_path + Path to save spot image tiles + library_id + Library id stored in AnnData. + crop_size + Size of tiles + verbose + Verbose output + copy + Return a copy instead of writing to adata. + target_size + Input size for convolutional neuron network + Returns + ------- + Depending on `copy`, returns or updates `adata` with the following fields. + **tile_path** : `adata.obs` field + Saved path for each spot image tiles + """ + + if library_id is None: + library_id = list(adata.uns["spatial"].keys())[0] + + # Check the exist of out_path + if not os.path.isdir(out_path): + os.mkdir(out_path) + + image = adata.uns["spatial"][library_id]["images"][ + adata.uns["spatial"][library_id]["use_quality"] + ] + if image.dtype == np.float32 or image.dtype == np.float64: + image = (image * 255).astype(np.uint8) + img_pillow = Image.fromarray(image) + + if img_pillow.mode == "RGBA": + img_pillow = img_pillow.convert("RGB") + + tile_names = [] + + with tqdm( + total=len(adata), + desc="Tiling image", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", + ) as pbar: + for imagerow, imagecol in zip(adata.obs["imagerow"], adata.obs["imagecol"]): + imagerow_down = imagerow - crop_size / 2 + imagerow_up = imagerow + crop_size / 2 + imagecol_left = imagecol - crop_size / 2 + imagecol_right = imagecol + crop_size / 2 + tile = img_pillow.crop( + (imagecol_left, imagerow_down, imagecol_right, imagerow_up) + ) + tile.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) + tile = tile.resize((target_size, target_size)) + tile_name = str(imagecol) + "-" + str(imagerow) + "-" + str(crop_size) + + if img_fmt == "JPEG": + out_tile = Path(out_path) / (tile_name + ".jpeg") + tile_names.append(str(out_tile)) + tile.save(out_tile, "JPEG") + else: + out_tile = Path(out_path) / (tile_name + ".png") + tile_names.append(str(out_tile)) + tile.save(out_tile, "PNG") + + if verbose: + print( + "generate tile at location ({}, {})".format( + str(imagecol), str(imagerow) + ) + ) + + pbar.update(1) + + adata.obs["tile_path"] = tile_names + return adata if copy else None def tiling( diff --git a/stlearn/pp.py b/stlearn/pp.py index b91eb648..596ab46c 100644 --- a/stlearn/pp.py +++ b/stlearn/pp.py @@ -1,5 +1,5 @@ from .image_preprocessing.feature_extractor import extract_feature, old_extract_feature -from .image_preprocessing.image_tiling import tiling +from .image_preprocessing.image_tiling import tiling, old_tiling from .preprocessing.filter_genes import filter_genes from .preprocessing.graph import neighbors from .preprocessing.log_scale import log1p, scale @@ -12,6 +12,7 @@ "scale", "neighbors", "tiling", + "old_tiling", "extract_feature", "old_extract_feature", ] From ee172e19e7ff49b44757cf3c357c14a34b34e1aa Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 14:42:04 +1000 Subject: [PATCH 074/241] Try. --- .github/workflows/python-package.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index cb7120fa..316304b1 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.10] steps: - uses: actions/checkout@v2 @@ -27,10 +27,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install pytest - - pip install leidenalg if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install louvain - name: Test with pytest run: | pytest From 2c996a50850145b3325bf38e9730aa51abdffe5c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 14:56:56 +1000 Subject: [PATCH 075/241] Fix style, remove old methods. --- stlearn/adds/parsing.py | 2 +- .../image_preprocessing/feature_extractor.py | 91 ++-------------- stlearn/image_preprocessing/image_tiling.py | 101 +----------------- stlearn/plotting/classes.py | 8 +- stlearn/pp.py | 6 +- stlearn/spatials/trajectory/utils.py | 11 +- stlearn/tools/clustering/louvain.py | 2 +- tests/test_cluster_plot.py | 68 ++++++------ tests/test_extract_features.py | 15 ++- tests/test_tiling.py | 23 ++-- 10 files changed, 77 insertions(+), 250 deletions(-) diff --git a/stlearn/adds/parsing.py b/stlearn/adds/parsing.py index 88847fbd..0ae6a9f0 100644 --- a/stlearn/adds/parsing.py +++ b/stlearn/adds/parsing.py @@ -29,7 +29,7 @@ def parsing( # Get a map of the new coordinates new_coordinates = dict() - with open(coordinates_file, mode='r') as filehandler: + with open(coordinates_file) as filehandler: for line in filehandler.readlines(): tokens = line.split() assert len(tokens) >= 6 or len(tokens) == 4 diff --git a/stlearn/image_preprocessing/feature_extractor.py b/stlearn/image_preprocessing/feature_extractor.py index 6f521ec8..a4cf5730 100644 --- a/stlearn/image_preprocessing/feature_extractor.py +++ b/stlearn/image_preprocessing/feature_extractor.py @@ -1,100 +1,23 @@ from typing import Literal import numpy as np -from PIL import Image from anndata import AnnData +from PIL import Image from sklearn.decomposition import PCA from tqdm import tqdm -import pandas as pd from .model_zoo import Model _CNN_BASE = Literal["resnet50", "vgg16", "inception_v3", "xception"] -def old_extract_feature( +def extract_feature( adata: AnnData, cnn_base: _CNN_BASE = "resnet50", n_components: int = 50, + seeds: int = 1, verbose: bool = False, copy: bool = False, - seeds: int = 1, -) -> AnnData | None: - """\ - Extract latent morphological features from H&E images using pre-trained - convolutional neural network base - - Parameters - ---------- - adata: - Annotated data matrix. - cnn_base: - Established convolutional neural network bases - choose one from ['resnet50', 'vgg16', 'inception_v3', 'xception'] - n_components: - Number of principal components to compute for latent morphological features - verbose: - Verbose output - copy: - Return a copy instead of writing to adata. - seeds: - Fix random state - Returns - ------- - Depending on `copy`, returns or updates `adata` with the following fields. - **X_morphology** : `adata.obsm` field - Dimension reduced latent morphological features. - """ - feature_dfs = [] - model = Model(cnn_base) - - if "tile_path" not in adata.obs: - raise ValueError("Please run the function stlearn.pp.tiling") - - def encode(tiles, model): - features = model.predict(tiles) - features = features.ravel() - return features - - with tqdm( - total=len(adata), - desc="Extract feature", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", - ) as pbar: - for spot, tile_path in adata.obs["tile_path"].items(): - tile = Image.open(tile_path) - tile = np.asarray(tile, dtype="int32") - tile = tile.astype(np.float32) - tile = np.stack([tile]) - if verbose: - print("extract feature for spot: {}".format(str(spot))) - features = encode(tile, model) - feature_dfs.append(pd.DataFrame(features, columns=[spot])) - pbar.update(1) - - feature_df = pd.concat(feature_dfs, axis=1) - - adata.obsm["X_tile_feature"] = feature_df.transpose().to_numpy() - - from sklearn.decomposition import PCA - - pca = PCA(n_components=n_components, random_state=seeds) - pca.fit(feature_df.transpose().to_numpy()) - - adata.obsm["X_morphology"] = pca.transform(feature_df.transpose().to_numpy()) - - print("The morphology feature is added to adata.obsm['X_morphology']!") - - return adata if copy else None - - -def extract_feature( - adata: AnnData, - cnn_base: _CNN_BASE = "resnet50", - n_components: int = 50, - seeds: int = 1, - verbose: bool = False, - copy: bool = False, ) -> AnnData | None: """\ Extract latent morphological features from H&E images using pre-trained @@ -147,10 +70,10 @@ def extract_feature( feature_matrix[0] = first_features with tqdm( - total=n_spots, - desc="Extract feature", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", - initial=1, # We already processed the first image + total=n_spots, + desc="Extract feature", + bar_format="{l_bar}{bar} [ time left: {remaining} ]", + initial=1, # We already processed the first image ) as pbar: for i in range(1, n_spots): features = _read_and_predict(tile_paths[i], model, verbose=verbose) diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index f1ffba94..302d3f23 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -1,104 +1,9 @@ -from typing import Optional, Union +from pathlib import Path + +import numpy as np from anndata import AnnData from PIL import Image -from pathlib import Path from tqdm import tqdm -import numpy as np -import os - - -def old_tiling( - adata: AnnData, - out_path: Union[Path, str] = "./tiling", - library_id: Union[str, None] = None, - crop_size: int = 40, - target_size: int = 299, - img_fmt: str = "JPEG", - verbose: bool = False, - copy: bool = False, -) -> Optional[AnnData]: - """\ - Tiling H&E images to small tiles based on spot spatial location - - Parameters - ---------- - adata - Annotated data matrix. - out_path - Path to save spot image tiles - library_id - Library id stored in AnnData. - crop_size - Size of tiles - verbose - Verbose output - copy - Return a copy instead of writing to adata. - target_size - Input size for convolutional neuron network - Returns - ------- - Depending on `copy`, returns or updates `adata` with the following fields. - **tile_path** : `adata.obs` field - Saved path for each spot image tiles - """ - - if library_id is None: - library_id = list(adata.uns["spatial"].keys())[0] - - # Check the exist of out_path - if not os.path.isdir(out_path): - os.mkdir(out_path) - - image = adata.uns["spatial"][library_id]["images"][ - adata.uns["spatial"][library_id]["use_quality"] - ] - if image.dtype == np.float32 or image.dtype == np.float64: - image = (image * 255).astype(np.uint8) - img_pillow = Image.fromarray(image) - - if img_pillow.mode == "RGBA": - img_pillow = img_pillow.convert("RGB") - - tile_names = [] - - with tqdm( - total=len(adata), - desc="Tiling image", - bar_format="{l_bar}{bar} [ time left: {remaining} ]", - ) as pbar: - for imagerow, imagecol in zip(adata.obs["imagerow"], adata.obs["imagecol"]): - imagerow_down = imagerow - crop_size / 2 - imagerow_up = imagerow + crop_size / 2 - imagecol_left = imagecol - crop_size / 2 - imagecol_right = imagecol + crop_size / 2 - tile = img_pillow.crop( - (imagecol_left, imagerow_down, imagecol_right, imagerow_up) - ) - tile.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) - tile = tile.resize((target_size, target_size)) - tile_name = str(imagecol) + "-" + str(imagerow) + "-" + str(crop_size) - - if img_fmt == "JPEG": - out_tile = Path(out_path) / (tile_name + ".jpeg") - tile_names.append(str(out_tile)) - tile.save(out_tile, "JPEG") - else: - out_tile = Path(out_path) / (tile_name + ".png") - tile_names.append(str(out_tile)) - tile.save(out_tile, "PNG") - - if verbose: - print( - "generate tile at location ({}, {})".format( - str(imagecol), str(imagerow) - ) - ) - - pbar.update(1) - - adata.obs["tile_path"] = tile_names - return adata if copy else None def tiling( diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 41450bae..343bdc93 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -689,8 +689,9 @@ def __init__( def _add_cluster_colors(self): self.adata[0].uns[self.use_label + "_colors"] = [] - for i, cluster in enumerate(self.adata[0].obs.groupby(self.use_label, - observed=True)): + for i, cluster in enumerate( + self.adata[0].obs.groupby(self.use_label, observed=True) + ): self.adata[0].uns[self.use_label + "_colors"].append( matplotlib.colors.to_hex(self.cmap_(i / (self.cmap_n - 1))) ) @@ -699,7 +700,8 @@ def _plot_clusters(self): # Plot scatter plot based on pixel of spots for i, cluster in enumerate( - self.query_adata.obs.groupby(self.use_label, observed=True)): + self.query_adata.obs.groupby(self.use_label, observed=True) + ): # Plot scatter plot based on pixel of spots subset_spatial = self.query_adata.obsm["spatial"][ check_sublist(list(self.query_adata.obs.index), list(cluster[1].index)) diff --git a/stlearn/pp.py b/stlearn/pp.py index 596ab46c..695efd24 100644 --- a/stlearn/pp.py +++ b/stlearn/pp.py @@ -1,5 +1,5 @@ -from .image_preprocessing.feature_extractor import extract_feature, old_extract_feature -from .image_preprocessing.image_tiling import tiling, old_tiling +from .image_preprocessing.feature_extractor import extract_feature +from .image_preprocessing.image_tiling import tiling from .preprocessing.filter_genes import filter_genes from .preprocessing.graph import neighbors from .preprocessing.log_scale import log1p, scale @@ -12,7 +12,5 @@ "scale", "neighbors", "tiling", - "old_tiling", "extract_feature", - "old_extract_feature", ] diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatials/trajectory/utils.py index ce99e9b5..d8cc4277 100644 --- a/stlearn/spatials/trajectory/utils.py +++ b/stlearn/spatials/trajectory/utils.py @@ -1,6 +1,7 @@ +import warnings + import networkx as nx import numpy as np -import warnings from numpy import linalg as la from scipy import linalg as spla from scipy import sparse as sps @@ -528,9 +529,7 @@ def _mat_mat_corr_sparse( y_std = np.reshape(np.std(Y, axis=0), (1, -1)) with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", r"invalid value encountered in true_divide" - ) + warnings.filterwarnings("ignore", r"invalid value encountered in true_divide") return (X @ Y - (n * X_bar * y_bar)) / ((n - 1) * X_std * y_std) @@ -604,9 +603,7 @@ def _mat_mat_corr_dense(X: np.ndarray, Y: np.ndarray) -> np.ndarray: y_std = np.reshape(np_std(Y, axis=0), (1, -1)) with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", r"invalid value encountered in true_divide" - ) + warnings.filterwarnings("ignore", r"invalid value encountered in true_divide") return (X @ Y - (n * X_bar * y_bar)) / ((n - 1) * X_std * y_std) diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tools/clustering/louvain.py index af28430f..78e973dd 100644 --- a/stlearn/tools/clustering/louvain.py +++ b/stlearn/tools/clustering/louvain.py @@ -97,4 +97,4 @@ def louvain( "Louvain cluster is done! The labels are stored in adata.obs['%s']" % key_added ) - return adata \ No newline at end of file + return adata diff --git a/tests/test_cluster_plot.py b/tests/test_cluster_plot.py index 4f04afcd..3d9280f8 100644 --- a/tests/test_cluster_plot.py +++ b/tests/test_cluster_plot.py @@ -3,13 +3,14 @@ """Tests for ClusterPlot.""" import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + +import matplotlib.colors import numpy as np import pandas as pd -import matplotlib.colors -import matplotlib.pyplot as plt from stlearn.plotting.classes import ClusterPlot + from .utils import read_test_data global adata @@ -25,30 +26,33 @@ def setUp(self): # Create test clustering data n_spots = len(self.adata.obs) - cluster_labels = np.random.choice(['Cluster_0', 'Cluster_1', 'Cluster_2'], - n_spots) - self.adata.obs['test_clusters'] = pd.Categorical(cluster_labels) + cluster_labels = np.random.choice( + ["Cluster_0", "Cluster_1", "Cluster_2"], n_spots + ) + self.adata.obs["test_clusters"] = pd.Categorical(cluster_labels) # Ensure we have a clean slate - if 'test_clusters_colors' in self.adata.uns: - del self.adata.uns['test_clusters_colors'] + if "test_clusters_colors" in self.adata.uns: + del self.adata.uns["test_clusters_colors"] def test_color_generation_first_call(self): """Test that colors are generated correctly on first call.""" - with patch('matplotlib.pyplot.subplots') as mock_subplots, \ - patch.object(ClusterPlot, '_plot_clusters') as mock_plot, \ - patch.object(ClusterPlot, '_add_image'): + with ( + patch("matplotlib.pyplot.subplots") as mock_subplots, + patch.object(ClusterPlot, "_plot_clusters") as _, + patch.object(ClusterPlot, "_add_image"), + ): # Mock matplotlib components mock_fig, mock_ax = MagicMock(), MagicMock() mock_subplots.return_value = (mock_fig, mock_ax) # Create ClusterPlot - label_name = 'test_clusters' + label_name = "test_clusters" plot = ClusterPlot( adata=self.adata, use_label=label_name, show_image=False, - show_color_bar=False + show_color_bar=False, ) # Check that colors were generated @@ -59,25 +63,27 @@ def test_color_generation_first_call(self): # Check that all colors are valid hex colors for color in colors: self.assertTrue(matplotlib.colors.is_color_like(color)) - self.assertTrue(color.startswith('#')) + self.assertTrue(color.startswith("#")) self.assertEqual(len(color), 7) # #RRGGBB format def test_multiple_calls_same_adata(self): """Test that multiple calls with same adata work correctly.""" - with patch('matplotlib.pyplot.subplots') as mock_subplots, \ - patch.object(ClusterPlot, '_plot_clusters') as mock_plot, \ - patch.object(ClusterPlot, '_add_image'): + with ( + patch("matplotlib.pyplot.subplots") as mock_subplots, + patch.object(ClusterPlot, "_plot_clusters") as _, + patch.object(ClusterPlot, "_add_image"), + ): mock_fig, mock_ax = MagicMock(), MagicMock() mock_subplots.return_value = (mock_fig, mock_ax) - label_name = 'test_clusters' + label_name = "test_clusters" # First call plot1 = ClusterPlot( adata=self.adata, use_label=label_name, show_image=False, - show_color_bar=False + show_color_bar=False, ) # Second call with same adata @@ -85,7 +91,7 @@ def test_multiple_calls_same_adata(self): adata=self.adata, use_label=label_name, show_image=False, - show_color_bar=False + show_color_bar=False, ) # Both should succeed and generate consistent colors @@ -98,13 +104,15 @@ def test_multiple_calls_same_adata(self): def test_insufficient_existing_colors_extended(self): """Test that insufficient existing colors are extended.""" # Pre-populate adata with insufficient colors (only 2 colors for 3 clusters) - existing_colors = ['#FF0000', '#00FF00'] - label_name = 'test_clusters' - self.adata.uns[f'{label_name}_colors'] = existing_colors - - with patch('matplotlib.pyplot.subplots') as mock_subplots, \ - patch.object(ClusterPlot, '_plot_clusters') as mock_plot, \ - patch.object(ClusterPlot, '_add_image'): + existing_colors = ["#FF0000", "#00FF00"] + label_name = "test_clusters" + self.adata.uns[f"{label_name}_colors"] = existing_colors + + with ( + patch("matplotlib.pyplot.subplots") as mock_subplots, + patch.object(ClusterPlot, "_plot_clusters") as _, + patch.object(ClusterPlot, "_add_image"), + ): mock_fig, mock_ax = MagicMock(), MagicMock() mock_subplots.return_value = (mock_fig, mock_ax) @@ -112,7 +120,7 @@ def test_insufficient_existing_colors_extended(self): adata=self.adata, use_label=label_name, show_image=False, - show_color_bar=False + show_color_bar=False, ) # Should extend existing colors @@ -123,5 +131,5 @@ def test_insufficient_existing_colors_extended(self): def tearDown(self): """Clean up after each test.""" # Clear any test artifacts - if hasattr(self, 'adata') and 'test_clusters_colors' in self.adata.uns: - del self.adata.uns['test_clusters_colors'] + if hasattr(self, "adata") and "test_clusters_colors" in self.adata.uns: + del self.adata.uns["test_clusters_colors"] diff --git a/tests/test_extract_features.py b/tests/test_extract_features.py index 6a6651f3..baaa2d06 100644 --- a/tests/test_extract_features.py +++ b/tests/test_extract_features.py @@ -1,12 +1,13 @@ #!/usr/bin/env python -import unittest -import numpy as np -import tempfile -import shutil import os +import shutil +import tempfile +import unittest +import numpy as np import scanpy as sc + import stlearn as st from .utils import read_test_data @@ -41,15 +42,14 @@ def test_deterministic_behavior(self): np.testing.assert_array_equal( data1.obsm["X_morphology"], data2.obsm["X_morphology"], - err_msg="Results should be deterministic with same seed" + err_msg="Results should be deterministic with same seed", ) np.testing.assert_array_equal( data1.obsm["X_tile_feature"], data2.obsm["X_tile_feature"], - err_msg="Results should be deterministic with same seed" + err_msg="Results should be deterministic with same seed", ) - def test_copy_behavior(self): """Test copy=True vs copy=False behavior.""" original_data = self.test_data.copy() @@ -64,4 +64,3 @@ def test_copy_behavior(self): result_inplace = st.pp.extract_feature(original_data, copy=False) self.assertIsNone(result_inplace) self.assertIn("X_morphology", original_data.obsm) - diff --git a/tests/test_tiling.py b/tests/test_tiling.py index 90fb2df8..fa6ce0cc 100644 --- a/tests/test_tiling.py +++ b/tests/test_tiling.py @@ -1,21 +1,15 @@ - # !/usr/bin/env python """Tests for tiling function.""" +import os +import shutil +import tempfile import unittest -import time -import numpy as np -import pandas as pd from pathlib import Path -import tempfile -import shutil -import os -from PIL import Image -from unittest.mock import patch, MagicMock -import filecmp -import scanpy as sc +import numpy as np + import stlearn as st from .utils import read_test_data @@ -61,8 +55,9 @@ def test_directory_creation(self): st.pp.tiling(data, nested_path) self.assertTrue(nested_path.exists(), "Nested directories not created") - self.assertGreater(len(list(nested_path.glob("*"))), 0, - "No files in nested directory") + self.assertGreater( + len(list(nested_path.glob("*"))), 0, "No files in nested directory" + ) def test_quality_parameter(self): """Test JPEG quality parameter.""" @@ -79,4 +74,4 @@ def test_quality_parameter(self): jpeg_files = list(Path(temp_quality).glob("*.jpeg")) self.assertEqual(len(jpeg_files), len(data)) - shutil.rmtree(temp_quality) \ No newline at end of file + shutil.rmtree(temp_quality) From 04b8f0d514046bb44c0ecfd16c3d7a5ae8b07b5d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 14:59:08 +1000 Subject: [PATCH 076/241] Update python-package.yml --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 316304b1..171f69f4 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.10] + python-version: [3.10.18] steps: - uses: actions/checkout@v2 From d6f1c0289326b68ef8a07be2fe21c6bce82f1c6a Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 15:04:23 +1000 Subject: [PATCH 077/241] Delete .github/workflows/pre-commit.yml Not needed. --- .github/workflows/pre-commit.yml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 72334791..00000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - branches: [master] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: pre-commit/action@v2.0.0 From cdc37a0e5916bcd9d1fc9bc270eea76935cb4fee Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 15:37:21 +1000 Subject: [PATCH 078/241] Fix documentation. --- stlearn/spatials/morphology/adjust.py | 29 +++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/stlearn/spatials/morphology/adjust.py b/stlearn/spatials/morphology/adjust.py index 8ec70950..2c68bcc2 100644 --- a/stlearn/spatials/morphology/adjust.py +++ b/stlearn/spatials/morphology/adjust.py @@ -6,16 +6,16 @@ from tqdm import tqdm _SIMILARITY_MATRIX = Literal["cosine", "euclidean", "pearson", "spearman"] - +_METHOD = Literal["mean", "median", "sum"] def adjust( adata: AnnData, use_data: str = "X_pca", radius: float = 50.0, rates: int = 1, - method="mean", - copy: bool = False, + method: _SIMILARITY_MATRIX = "mean", similarity_matrix: _SIMILARITY_MATRIX = "cosine", + copy: bool = False, ) -> AnnData | None: """\ SME normalisation: Using spot location information and tissue morphological @@ -23,23 +23,22 @@ def adjust( Parameters ---------- - adata + adata : AnnData Annotated data matrix. - use_data + use_data : str, default "X_pca" Input date to be adjusted by morphological features. choose one from ["raw", "X_pca", "X_umap"] - radius + radius: float, default 50.0 Radius to select neighbour spots. - rates - Strength for adjustment. - method - Method for disk smoothing. - choose one from ["means", "median"] - copy + rates: int, default 1 + Number of times to add the aggregated neighbor contribution. + Higher values increase the strength of morphological adjustment. + method: {'mean', 'median', 'sum'}, default 'mean' + Method for aggregating neighbor contributions. + similarity_matrix : {'cosine', 'euclidean', 'pearson', 'spearman'}, default 'cosine' + Method to calculate morphological similarity between spots. + copy : bool, default False Return a copy instead of writing to adata. - similarity_matrix - Matrix to calculate morphological similarity of two spots - choose one from ["cosine", "euclidean", "pearson", "spearman"] Returns ------- Depending on `copy`, returns or updates `adata` with the following fields. From d980bdadd636b069023f726bfe290928aeffc79f Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 12 Jun 2025 15:42:51 +1000 Subject: [PATCH 079/241] Update. --- .github/workflows/pre-commit.yml | 14 -------------- .github/workflows/python-package.yml | 7 ++----- 2 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 72334791..00000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - branches: [master] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: pre-commit/action@v2.0.0 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index cb7120fa..3f862acd 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.10.18] steps: - uses: actions/checkout@v2 @@ -27,10 +27,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install pytest - - pip install leidenalg if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install louvain - name: Test with pytest run: | - pytest + pytest \ No newline at end of file From 49aa9c624f5eeea8f8ba5f3e67ac5d737a7f92f1 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 13 Jun 2025 16:46:13 +1000 Subject: [PATCH 080/241] Add more quality control steps. --- .github/workflows/python-package.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3f862acd..5f5cc50d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -28,6 +28,13 @@ jobs: python -m pip install --upgrade pip python -m pip install pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Check style + run: | + black stlearn tests + ruff check stlearn tests + - name: Check types + run: | + mypy stlearn tests - name: Test with pytest run: | pytest \ No newline at end of file From 45cb689fa921b5696e69f18788f0488de1e66839 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sun, 15 Jun 2025 09:45:18 +1000 Subject: [PATCH 081/241] Add some checks and refactor into methods. --- stlearn/image_preprocessing/image_tiling.py | 140 ++++++++++++-------- 1 file changed, 85 insertions(+), 55 deletions(-) diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index 302d3f23..547d4877 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -13,33 +13,34 @@ def tiling( crop_size: int = 40, target_size: int = 299, img_fmt: str = "JPEG", - quality: int = 95, + quality: int = 75, verbose: bool = False, copy: bool = False, ) -> AnnData | None: """\ - Tiling H&E images to small tiles based on spot spatial location + Tiling H&E images to small tiles based on spot spatial location. Parameters ---------- - adata: - Annotated data matrix. - out_path: - Path to save spot image tiles - library_id: - Library id stored in AnnData. - crop_size: - Size of tiles - target_size: - Input size for convolutional neuron network - img_fmt: - Image format ('JPEG' or 'PNG') - quality: - JPEG quality 1-100. - verbose: - Verbose output - copy: - Return a copy instead of writing to adata. + adata: AnnData + Annotated data matrix containing spatial information. + out_path: Path or str, default "./tiling" + Path to save spot image tiles. + library_id: str, optional + Library id stored in AnnData. If None, uses first available library. + crop_size: int, default 40 + Size of tiles to crop from original image. + target_size: int, default 299 + Target size for resized tiles (input size for CNN). + img_fmt: str, default "JPEG" + Image format ('JPEG' or 'PNG'). + quality: int, default 75 + JPEG quality (1-100). Only used for JPEG format. + verbose: bool, default False + Enable verbose output. + copy: bool, default False + Return a copy instead of modifying adata in-place. + Returns ------- Depending on `copy`, returns or updates `adata` with the following fields. @@ -47,39 +48,17 @@ def tiling( Saved path for each spot image tiles """ - adata = adata.copy() if copy else adata - - if not isinstance(crop_size, int) or crop_size <= 0: - raise ValueError("crop_size must be a positive integer") - if not isinstance(target_size, int) or target_size <= 0: - raise ValueError("target_size must be a positive integer") - if img_fmt.upper() not in ["JPEG", "PNG"]: - raise ValueError("img_fmt must be 'JPEG' or 'PNG'") + _validate_inputs(crop_size, target_size, img_fmt, quality) - if library_id is None: - library_id = list(adata.uns["spatial"].keys())[0] + adata = adata.copy() if copy else adata out_path = Path(out_path) out_path.mkdir(parents=True, exist_ok=True) - # Load and prepare image - try: - image = adata.uns["spatial"][library_id]["images"][ - adata.uns["spatial"][library_id]["use_quality"] - ] - except KeyError as e: - raise ValueError(f"Could not find image data in adata.uns['spatial']: {e}") + library_id = _get_library_id(adata, library_id) + img_pillow = _load_and_prepare_image(adata, library_id) - if image.dtype in (np.float32, np.float64): - image = np.clip(image, 0, 1) - image = (image * 255).astype(np.uint8) - - img_pillow = Image.fromarray(image) - - if img_pillow.mode == "RGBA": - img_pillow = img_pillow.convert("RGB") - - coordinates = list(zip(adata.obs["imagerow"], adata.obs["imagecol"])) + coordinates = list(zip(adata.obs["image_row"], adata.obs["image_col"])) tile_names = [] @@ -88,20 +67,20 @@ def tiling( desc="Tiling image", bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: - for imagerow, imagecol in coordinates: + for image_row, image_col in coordinates: half_crop = crop_size // 2 - imagerow_down = max(0, imagerow - half_crop) - imagerow_up = imagerow + half_crop - imagecol_left = max(0, imagecol - half_crop) - imagecol_right = imagecol + half_crop + image_row_down = max(0, image_row - half_crop) + image_row_up = image_row + half_crop + image_col_left = max(0, image_col - half_crop) + image_col_right = image_col + half_crop tile = img_pillow.crop( - (imagecol_left, imagerow_down, imagecol_right, imagerow_up) + (image_col_left, image_row_down, image_col_right, image_row_up) ) tile.thumbnail((target_size, target_size), Image.Resampling.LANCZOS) tile = tile.resize((target_size, target_size)) - tile_name = str(imagecol) + "-" + str(imagerow) + "-" + str(crop_size) + tile_name = str(image_col) + "-" + str(image_row) + "-" + str(crop_size) if img_fmt == "JPEG": out_tile = Path(out_path) / (tile_name + ".jpeg") @@ -113,9 +92,60 @@ def tiling( tile.save(out_tile, "PNG") if verbose: - print(f"generate tile at location ({str(imagecol)}, {str(imagerow)})") + print(f"generate tile at location ({str(image_col)}, {str(image_row)})") pbar.update(1) adata.obs["tile_path"] = tile_names return adata if copy else None + + +def _validate_inputs(crop_size: int, target_size: int, img_fmt: str, + quality: int) -> None: + + if not isinstance(crop_size, int) or crop_size <= 0: + raise ValueError("crop_size must be a positive integer") + + if not isinstance(target_size, int) or target_size <= 0: + raise ValueError("target_size must be a positive integer") + + if img_fmt.upper() not in ["JPEG", "PNG"]: + raise ValueError("img_fmt must be 'JPEG' or 'PNG'") + + if img_fmt.upper() == "JPEG" and ( + not isinstance(quality, int) or not 1 <= quality <= 100): + raise ValueError("quality must be an integer between 1 and 100 for JPEG format") + + +def _get_library_id(adata: AnnData, library_id: str | None) -> str: + if library_id is None: + try: + library_id = list(adata.uns["spatial"].keys())[0] + except (KeyError, IndexError): + raise ValueError("No spatial data found in adata.uns['spatial']") + + if library_id not in adata.uns["spatial"]: + raise ValueError(f"Library '{library_id}' not found in spatial data") + + return library_id + + +def _load_and_prepare_image(adata: AnnData, library_id: str) -> Image.Image: + try: + spatial_data = adata.uns["spatial"][library_id] + use_quality = spatial_data["use_quality"] + image = spatial_data["images"][use_quality] + except KeyError as e: + raise ValueError( + f"Could not find image data in adata.uns['spatial']['{library_id}']: {e}") + + if image.dtype in (np.float32, np.float64): + image = np.clip(image, 0, 1) + image = (image * 255).astype(np.uint8) + + img_pillow = Image.fromarray(image) + + if img_pillow.mode == "RGBA": + img_pillow = img_pillow.convert("RGB") + + return img_pillow \ No newline at end of file From a07552f7948b7072f9b9b8adcb9783ecd2e4e320 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 23 Jun 2025 13:54:58 +1000 Subject: [PATCH 082/241] Small fixes. --- stlearn/spatials/clustering/localization.py | 15 ++- stlearn/spatials/trajectory/local_level.py | 14 +- .../spatials/trajectory/pseudotimespace.py | 31 +++-- .../trajectory/weight_optimization.py | 122 ++++++++++-------- 4 files changed, 99 insertions(+), 83 deletions(-) diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index 1fd17129..a599d4e1 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -17,19 +17,20 @@ def localization( Parameters ---------- - adata + adata: AnnData Annotated data matrix. - use_label + use_label: str, default = "louvain" Use label result of cluster method. - eps + eps: The maximum distance between two samples for one to be considered as in the neighborhood of the other. This is not a maximum bound on the distances of points within a cluster. This is the most important DBSCAN parameter to choose appropriately for your data set and distance function. - min_samples + min_samples: The number of samples (or total weight) in a neighborhood for a point to be - considered as a core point. This includes the point itself. - copy + considered as a core point. This includes the point itself. Passed into DBSCAN's + min_samples parameter. + copy: Return a copy instead of writing to adata. Returns ------- @@ -46,7 +47,7 @@ def localization( for i in adata.obs[use_label].unique(): tmp = adata.obs[adata.obs[use_label] == i] - clustering = DBSCAN(eps=eps, min_samples=1, algorithm="kd_tree").fit( + clustering = DBSCAN(eps=eps, min_samples=min_samples, algorithm="kd_tree").fit( tmp[["imagerow", "imagecol"]] ) diff --git a/stlearn/spatials/trajectory/local_level.py b/stlearn/spatials/trajectory/local_level.py index a0490b88..56f1b3ec 100644 --- a/stlearn/spatials/trajectory/local_level.py +++ b/stlearn/spatials/trajectory/local_level.py @@ -16,18 +16,18 @@ def local_level( Parameters ---------- - adata + adata: Annotated data matrix. - use_label + use_label: Use label result of cluster method. - cluster + cluster: Choose cluster to perform local spatial trajectory inference. - threshold - Threshold to find the significant connection for PAGA graph. - w + w: float, default=0.5 Pseudo-spatio-temporal distance weight (balance between spatial effect and DPT) - return_matrix + return_matrix: Return PTS matrix for local level + verbose : bool, default=True + Whether to print progress information. Returns ------- np.ndarray: the STDM (spatio-temporal distance matrix) - weighted combination of diff --git a/stlearn/spatials/trajectory/pseudotimespace.py b/stlearn/spatials/trajectory/pseudotimespace.py index acd52725..60573d10 100644 --- a/stlearn/spatials/trajectory/pseudotimespace.py +++ b/stlearn/spatials/trajectory/pseudotimespace.py @@ -1,3 +1,5 @@ +from typing import Literal + from anndata import AnnData from .global_level import global_level @@ -11,7 +13,7 @@ def pseudotimespace_global( use_rep: str = "X_pca", n_dims: int = 40, list_clusters=None, - model: str = "spatial", + model: Literal["spatial", "gene_expression", "mixed"] = "spatial", step=0.01, k=10, ) -> AnnData | None: @@ -20,23 +22,24 @@ def pseudotimespace_global( Parameters ---------- - adata: + adata: AnnData Annotated data matrix. - use_label: + use_label: str, default = "louvain" Use label result of cluster method. - use_rep: + use_rep: str, default = "X_pca" Which obsm location to use. - n_dims: + n_dims: int, default = 40 Number of dimensions to use in PCA - list_clusters: - List of cluster used to reconstruct spatial trajectory. - model: + list_clusters: list, optional + List of cluster used to reconstruct spatial trajectory. If None, uses all + clusters. + model: Literal["spatial", "gene_expression", "mixed"] = "mixed", Can be mixed, spatial or gene expression. spatial sets weight to 0, gene expression sets weight to 1 and mixed uses the list_clusters, step and k. - step: - Step for screening weighting factor - k - The number of eigenvalues to be compared + step: float, default = 0.01 + Step for screening weighting factor. + k: int, default = 10 + The number of eigenvalues to be compared. Returns ------- Anndata @@ -82,9 +85,9 @@ def pseudotimespace_local( Parameters ---------- - adata: + adata: AnnData Annotated data matrix. - use_label: + use_label: str, default = "louvain" Use label result of cluster method. cluster: Cluster used to reconstruct intra regional spatial trajectory. diff --git a/stlearn/spatials/trajectory/weight_optimization.py b/stlearn/spatials/trajectory/weight_optimization.py index 9787d628..ff4e7385 100644 --- a/stlearn/spatials/trajectory/weight_optimization.py +++ b/stlearn/spatials/trajectory/weight_optimization.py @@ -1,6 +1,7 @@ import networkx as nx import numpy as np import pandas as pd +from anndata import AnnData from tqdm import tqdm from .global_level import global_level @@ -9,40 +10,62 @@ def weight_optimizing_global( - adata, - use_label=None, + adata: AnnData, + use_label: str = "louvain", list_clusters=None, step=0.01, k=10, use_rep="X_pca", n_dims=40, ): + if k <= 0: + raise ValueError(f"k must be positive, got {k}") + + # Determine effective k value based on available sub-clusters + actual_k = k + if use_label and list_clusters: + if "sub_cluster_labels" not in adata.obs.columns: + print("Warning: 'sub_cluster_labels' column not found. Using provided " + + "k value.") + else: + try: + filtered_data = adata.obs[adata.obs[use_label].isin(list_clusters)] + if len(filtered_data) == 0: + raise ValueError(f"No cells found for clusters {list_clusters} " + + "in column '{use_label}'") + + # Minimum 1 cluster, use K or max available sub-clusters + n_subclusters = len(filtered_data["sub_cluster_labels"].unique()) + actual_k = max(1, min(k, n_subclusters)) + + if actual_k != k: + print(f"Adjusted k from {k} to {actual_k} based on available " + + "sub-clusters ({n_subclusters})") + + except Exception as e: + print(f"Warning: Could not determine sub-cluster count: {e}. " + + "Using provided k value.") + actual_k = k + # Screening PTS graph print("Screening PTS global graph...") Gs = [] j = 0 - + total_iterations = int(1 / step + 1) with tqdm( - total=int(1 / step + 1), + total=total_iterations, desc="Screening", bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: - for i in range(0, int(1 / step + 1)): + for i in range(0, total_iterations): + weight = round(i * step, 2) + matrix = global_level(adata, use_label=use_label, + list_clusters=list_clusters, use_rep=use_rep, + n_dims=n_dims, w=weight, return_graph=True, + verbose=False, ) Gs.append( - nx.to_scipy_sparse_array( - global_level( - adata, - use_label=use_label, - list_clusters=list_clusters, - use_rep=use_rep, - n_dims=n_dims, - w=round(j, 2), - return_graph=True, - verbose=False, - ) - ) + nx.to_scipy_sparse_array(matrix) ) - j = j + step pbar.update(1) @@ -51,13 +74,8 @@ def weight_optimizing_global( result = [] a1_list = [] a2_list = [] - indx = [] + index = [] w = 0 - k = len( - adata.obs[adata.obs[use_label].isin(list_clusters)][ - "sub_cluster_labels" - ].unique() - ) with tqdm( total=int(1 / step - 1), desc="Calculating", @@ -65,28 +83,25 @@ def weight_optimizing_global( ) as pbar: for i in range(1, int(1 / step)): w += step - a1 = lambda_dist(Gs[i], Gs[0], k=k) - a2 = lambda_dist(Gs[i], Gs[-1], k=k) + a1 = lambda_dist(Gs[i], Gs[0], k=actual_k) + a2 = lambda_dist(Gs[i], Gs[-1], k=actual_k) a1_list.append(a1) a2_list.append(a2) - indx.append(w) + index.append(w) result.append(np.absolute(1 - a1 / a2)) pbar.update(1) screening_result = pd.DataFrame( - {"w": indx, "A1": a1_list, "A2": a2_list, "Dissmilarity_Score": result} + {"w": index, "A1": a1_list, "A2": a2_list, "Dissmilarity_Score": result} ) adata.uns["screening_result_global"] = screening_result - def NormalizeData(data): - return (data - np.min(data)) / (np.max(data) - np.min(data)) - - result = NormalizeData(result) + normalised_result = normalize_data(result) try: - optimized_ind = np.where(result == np.amin(result))[0][0] - opt_w = round(indx[optimized_ind], 2) + optimized_ind = np.where(normalised_result == np.amin(normalised_result))[0][0] + opt_w = round(index[optimized_ind], 2) print("The optimized weighting is:", str(opt_w)) return opt_w except: @@ -94,7 +109,10 @@ def NormalizeData(data): return 0.5 -def weight_optimizing_local(adata, use_label=None, cluster=None, step=0.01): +def weight_optimizing_local(adata: AnnData, + use_label: str = "louvain", + cluster=None, + step=0.01): # Screening PTS graph print("Screening PTS local graph...") Gs = [] @@ -105,17 +123,9 @@ def weight_optimizing_local(adata, use_label=None, cluster=None, step=0.01): bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(0, int(1 / step + 1)): - Gs.append( - local_level( - adata, - use_label=use_label, - cluster=cluster, - w=round(j, 2), - verbose=False, - return_matrix=True, - ) - ) - + matrix = local_level(adata, use_label=use_label, cluster=cluster, + w=round(j, 2), verbose=False, return_matrix=True) + Gs.append(matrix) j = j + step pbar.update(1) @@ -124,7 +134,7 @@ def weight_optimizing_local(adata, use_label=None, cluster=None, step=0.01): result = [] a1_list = [] a2_list = [] - indx = [] + index = [] w = 0 with tqdm( @@ -138,23 +148,25 @@ def weight_optimizing_local(adata, use_label=None, cluster=None, step=0.01): a2 = resistance_distance(Gs[i], Gs[-1]) a1_list.append(a1) a2_list.append(a2) - indx.append(w) + index.append(w) result.append(np.absolute(1 - a1 / a2)) pbar.update(1) screening_result = pd.DataFrame( - {"w": indx, "A1": a1_list, "A2": a2_list, "Dissmilarity_Score": result} + {"w": index, "A1": a1_list, "A2": a2_list, "Dissmilarity_Score": result} ) adata.uns["screening_result_local"] = screening_result - def NormalizeData(data): - return (data - np.min(data)) / (np.max(data) - np.min(data)) - - result = NormalizeData(result) + normalised_result = normalize_data(result) - optimized_ind = np.where(result == np.amin(result))[0][0] - opt_w = round(indx[optimized_ind], 2) + optimized_ind = np.where(normalised_result == np.amin(normalised_result))[0][0] + opt_w = round(index[optimized_ind], 2) print("The optimized weighting is:", str(opt_w)) return opt_w + + +def normalize_data(data): + return (data - np.min(data)) / (np.max(data) - np.min(data)) + From e705862b1ebda6d8cb1829829161f2715cb99c61 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 23 Jun 2025 16:02:14 +1000 Subject: [PATCH 083/241] Add filter_cells wrapper. --- stlearn/pp.py | 2 + stlearn/preprocessing/filter_cells.py | 62 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 stlearn/preprocessing/filter_cells.py diff --git a/stlearn/pp.py b/stlearn/pp.py index 695efd24..5e5d4df0 100644 --- a/stlearn/pp.py +++ b/stlearn/pp.py @@ -1,11 +1,13 @@ from .image_preprocessing.feature_extractor import extract_feature from .image_preprocessing.image_tiling import tiling from .preprocessing.filter_genes import filter_genes +from .preprocessing.filter_cells import filter_cells from .preprocessing.graph import neighbors from .preprocessing.log_scale import log1p, scale from .preprocessing.normalize import normalize_total __all__ = [ + "filter_cells", "filter_genes", "normalize_total", "log1p", diff --git a/stlearn/preprocessing/filter_cells.py b/stlearn/preprocessing/filter_cells.py new file mode 100644 index 00000000..6736c5a0 --- /dev/null +++ b/stlearn/preprocessing/filter_cells.py @@ -0,0 +1,62 @@ +import numpy as np +import scanpy +from anndata import AnnData + + +def filter_cells( + adata: AnnData, + min_counts: int | None = None, + min_genes: int | None = None, + max_counts: int | None = None, + max_genes: int | None = None, + inplace: bool = True, +) -> AnnData | None | tuple[np.ndarray, np.ndarray]: + """\ + Wrap function scanpy.pp.filter_cells + + Filter cell outliers based on counts and numbers of genes expressed. + + For instance, only keep cells with at least `min_counts` counts or + `min_genes` genes expressed. This is to filter measurement outliers, + i.e. “unreliable” observations. + + Only provide one of the optional parameters `min_counts`, `min_genes`, + `max_counts`, `max_genes` per call. + + Parameters + ---------- + adata + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + min_counts + Minimum number of counts required for a cell to pass filtering. + min_genes + Minimum number of genes expressed required for a cell to pass filtering. + max_counts + Maximum number of counts required for a cell to pass filtering. + max_genes + Maximum number of genes expressed required for a cell to pass filtering. + inplace + Perform computation inplace or return result. + + Returns + ------- + Depending on `inplace`, returns the following arrays or directly subsets + and annotates the data matrix: + + cells_subset + Boolean index mask that does filtering. `True` means that the + cell is kept. `False` means the cell is removed. + number_per_cell + Depending on what was thresholded (`counts` or `genes`), + the array stores `n_counts` or `n_cells` per gene. + """ + + return scanpy.pp.filter_cells( + adata, + min_counts=min_counts, + min_genes=min_genes, + max_counts=max_counts, + max_genes=max_genes, + inplace=inplace, + ) From abad4f5d3edc08c3519ccd91c5aad4823bd9c0e3 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 23 Jun 2025 16:33:55 +1000 Subject: [PATCH 084/241] Fix default parameters and add documentation. --- stlearn/spatials/clustering/localization.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index a599d4e1..f2594454 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -8,8 +8,8 @@ def localization( adata: AnnData, use_label: str = "louvain", - eps: float = 20, - min_samples: int = 0, + eps: float = 20.0, + min_samples: int = 1, copy: bool = False, ) -> AnnData | None: """\ @@ -21,16 +21,16 @@ def localization( Annotated data matrix. use_label: str, default = "louvain" Use label result of cluster method. - eps: + eps: float, default 20.0 The maximum distance between two samples for one to be considered as in the neighborhood of the other. This is not a maximum bound on the distances of points within a cluster. This is the most important DBSCAN parameter to choose appropriately for your data set and distance function. - min_samples: + min_samples: int, default = 1 The number of samples (or total weight) in a neighborhood for a point to be considered as a core point. This includes the point itself. Passed into DBSCAN's min_samples parameter. - copy: + copy: bool, default = False Return a copy instead of writing to adata. Returns ------- From 472e3c7a03eeb54d8aaaf9d49a788dca223d7188 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 1 Jul 2025 11:09:28 +1000 Subject: [PATCH 085/241] Renamed keys by mistake. --- stlearn/image_preprocessing/image_tiling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index 547d4877..34e38ce2 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -58,7 +58,7 @@ def tiling( library_id = _get_library_id(adata, library_id) img_pillow = _load_and_prepare_image(adata, library_id) - coordinates = list(zip(adata.obs["image_row"], adata.obs["image_col"])) + coordinates = list(zip(adata.obs["imagerow"], adata.obs["imagecol"])) tile_names = [] From ffaa7bbad420d542bd6843c66bbb79c35196d073 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 1 Jul 2025 11:18:33 +1000 Subject: [PATCH 086/241] Rename release to 0.5.0 and add to history. --- HISTORY.rst | 11 +++++++++++ pyproject.toml | 2 +- stlearn/__init__.py | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 39a6759c..5ec9e16e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,17 @@ History ======= +0.5.0 (2025-07-01) +------------------ +* Support Python 3.10.x +* Added quality checks black, ruff and mypy and fixed appropriate source code. +* Copy parameters now work with the same semantics as scanpy. +* Library upgrades for leidenalg, louvain, numba, numpy, scanpy, and tensorflow. + +API and Bug Fixes: +* Consistent with type annotations - mainly missing None annotations. +* pl.cluster_plot - Does not keep colours from previous runs when clustering. + 0.4.11 (2022-11-25) ------------------ 0.4.10 (2022-11-22) diff --git a/pyproject.toml b/pyproject.toml index 3b599795..8478ada2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "0.4.2" +version = "0.5.0" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 9736f217..3ba30867 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -2,7 +2,7 @@ __author__ = """Genomics and Machine Learning lab""" __email__ = "andrew.newman@uq.edu.au" -__version__ = "0.4.2" +__version__ = "0.5.0" from . import add, datasets, em, pl, pp, spatial, tl from ._settings import settings From 2533fc4119ed4bfcbb0ec7ab4dcaebb5c8b6615e Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 1 Jul 2025 12:08:34 +1000 Subject: [PATCH 087/241] Add check for available clusters and improve documentation. --- stlearn/spatials/trajectory/set_root.py | 28 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/stlearn/spatials/trajectory/set_root.py b/stlearn/spatials/trajectory/set_root.py index 6d232589..30cf3ae7 100644 --- a/stlearn/spatials/trajectory/set_root.py +++ b/stlearn/spatials/trajectory/set_root.py @@ -6,26 +6,38 @@ def set_root(adata: AnnData, use_label: str, cluster: str, use_raw: bool = False): """\ - Automatically set the root index. + Automatically set the root index for trajectory analysis. Parameters ---------- - adata + adata: AnnData Annotated data matrix. - use_label + use_label: str Use label result of cluster method. - cluster - Choose cluster to use as root - use_raw - Use the raw layer + cluster: str + Cluster identifier to use as the root cluster. Must exist in + `adata.obs[use_label]`. Will be converted to string for comparison. + use_raw: bool, default False + If True, use `adata.raw.X` for calculations; otherwise use `adata.X`. Returns ------- - Root index + int + Index of the selected root cell in the AnnData object + Raises + ------ + ValueError + If the specified cluster is not found in the clustering results. + ZeroDivisionError + If the specified cluster contains no cells. """ tmp_adata = adata.copy() # Subset the data based on the chosen cluster + available_clusters = tmp_adata.obs[use_label].unique() + if str(cluster) not in available_clusters.astype(str): + raise ValueError(f"Cluster '{cluster}' not found in available clusters: " + + "{sorted(available_clusters)}") tmp_adata = tmp_adata[ tmp_adata.obs[tmp_adata.obs[use_label] == str(cluster)].index, : From 2842609ab0d0738382de763ac6f3476d22b0dc42 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 1 Jul 2025 15:06:05 +1000 Subject: [PATCH 088/241] If list_clusters is empty just use all clusters. --- stlearn/spatials/trajectory/global_level.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatials/trajectory/global_level.py index 21d4a3fc..16308966 100644 --- a/stlearn/spatials/trajectory/global_level.py +++ b/stlearn/spatials/trajectory/global_level.py @@ -50,9 +50,13 @@ def global_level( inds_cat = {v: k for (k, v) in cat_inds.items()} # Query cluster - if isinstance(list_clusters[0], str): - list_clusters = [cat_inds[label] for label in list_clusters] - query_nodes = list_clusters + if len(list_clusters) == 0: + print("No clusters specified, using all available clusters") + query_nodes = list(cat_inds.values()) + else: + if isinstance(list_clusters[0], str): + list_clusters = [cat_inds[label] for label in list_clusters] + query_nodes = list_clusters query_nodes = ordering_nodes(query_nodes, use_label, adata) if verbose: From cc3cf66a46322512d95ca07e18edc848b0d2ba80 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 09:07:55 +1000 Subject: [PATCH 089/241] Set boto3 version. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 4c11dc46..96ae684b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ bokeh==3.7.3 +boto3==1.38.45 click==8.2.1 leidenalg==0.10.2 louvain==0.8.2 From 7065054f6f9bfc74258eec0e9259ecf3bf84db94 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 09:11:15 +1000 Subject: [PATCH 090/241] Remove. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 96ae684b..4c11dc46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ bokeh==3.7.3 -boto3==1.38.45 click==8.2.1 leidenalg==0.10.2 louvain==0.8.2 From c6ae6b30055ffbcf8d3110391df656640ab80bfc Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 10:52:57 +1000 Subject: [PATCH 091/241] Remove double check of quants being np.array --- stlearn/tools/microenv/cci/perm_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index 869e5894..28b33ac4 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -12,7 +12,7 @@ def nonzero_quantile(expr, q, interpolation): """Calculating the non-zero quantiles.""" nonzero_expr = expr[expr > 0] quants = np.quantile(nonzero_expr, q=q, interpolation=interpolation) - if quants is not np.array and quants is not np.ndarray: + if quants is not np.array: quants = np.array([quants]) return quants From 3297fd69703273f256956ff380d8a5002912972c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 11:07:00 +1000 Subject: [PATCH 092/241] Fix type check. --- stlearn/tools/microenv/cci/perm_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index 28b33ac4..cd137ba1 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -12,7 +12,7 @@ def nonzero_quantile(expr, q, interpolation): """Calculating the non-zero quantiles.""" nonzero_expr = expr[expr > 0] quants = np.quantile(nonzero_expr, q=q, interpolation=interpolation) - if quants is not np.array: + if not isinstance(quants, np.ndarray) or quants.ndim == 0: quants = np.array([quants]) return quants From f455039dc60ab8d4c9f97cfcd8f983d8a2bf1d6a Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 11:27:28 +1000 Subject: [PATCH 093/241] Fixed up more type checks and created external types. --- stlearn/__init__.py | 3 +- stlearn/image_preprocessing/image_tiling.py | 13 +++-- stlearn/pp.py | 2 +- stlearn/spatials/morphology/adjust.py | 8 +-- stlearn/spatials/trajectory/pseudotime.py | 4 +- stlearn/spatials/trajectory/set_root.py | 6 +- .../trajectory/weight_optimization.py | 58 ++++++++++++------- stlearn/tools/label/label.py | 10 ++-- stlearn/tools/microenv/cci/analysis.py | 2 +- stlearn/tools/microenv/cci/base.py | 5 +- stlearn/tools/microenv/cci/base_grouping.py | 2 +- stlearn/tools/microenv/cci/perm_utils.py | 4 +- stlearn/tools/microenv/cci/permutation.py | 6 +- stlearn/types.py | 6 ++ 14 files changed, 81 insertions(+), 48 deletions(-) create mode 100644 stlearn/types.py diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 3ba30867..6fe7d20f 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -4,7 +4,7 @@ __email__ = "andrew.newman@uq.edu.au" __version__ = "0.5.0" -from . import add, datasets, em, pl, pp, spatial, tl +from . import add, datasets, em, pl, pp, spatial, tl, types from ._settings import settings from .wrapper.concatenate_spatial_adata import concatenate_spatial_adata from .wrapper.convert_scanpy import convert_scanpy @@ -37,6 +37,7 @@ "ReadXenium", "create_stlearn", "settings", + "types", "convert_scanpy", "concatenate_spatial_adata", ] diff --git a/stlearn/image_preprocessing/image_tiling.py b/stlearn/image_preprocessing/image_tiling.py index 34e38ce2..73f8b4b7 100644 --- a/stlearn/image_preprocessing/image_tiling.py +++ b/stlearn/image_preprocessing/image_tiling.py @@ -100,8 +100,9 @@ def tiling( return adata if copy else None -def _validate_inputs(crop_size: int, target_size: int, img_fmt: str, - quality: int) -> None: +def _validate_inputs( + crop_size: int, target_size: int, img_fmt: str, quality: int +) -> None: if not isinstance(crop_size, int) or crop_size <= 0: raise ValueError("crop_size must be a positive integer") @@ -113,7 +114,8 @@ def _validate_inputs(crop_size: int, target_size: int, img_fmt: str, raise ValueError("img_fmt must be 'JPEG' or 'PNG'") if img_fmt.upper() == "JPEG" and ( - not isinstance(quality, int) or not 1 <= quality <= 100): + not isinstance(quality, int) or not 1 <= quality <= 100 + ): raise ValueError("quality must be an integer between 1 and 100 for JPEG format") @@ -137,7 +139,8 @@ def _load_and_prepare_image(adata: AnnData, library_id: str) -> Image.Image: image = spatial_data["images"][use_quality] except KeyError as e: raise ValueError( - f"Could not find image data in adata.uns['spatial']['{library_id}']: {e}") + f"Could not find image data in adata.uns['spatial']['{library_id}']: {e}" + ) if image.dtype in (np.float32, np.float64): image = np.clip(image, 0, 1) @@ -148,4 +151,4 @@ def _load_and_prepare_image(adata: AnnData, library_id: str) -> Image.Image: if img_pillow.mode == "RGBA": img_pillow = img_pillow.convert("RGB") - return img_pillow \ No newline at end of file + return img_pillow diff --git a/stlearn/pp.py b/stlearn/pp.py index 5e5d4df0..9a191237 100644 --- a/stlearn/pp.py +++ b/stlearn/pp.py @@ -1,7 +1,7 @@ from .image_preprocessing.feature_extractor import extract_feature from .image_preprocessing.image_tiling import tiling -from .preprocessing.filter_genes import filter_genes from .preprocessing.filter_cells import filter_cells +from .preprocessing.filter_genes import filter_genes from .preprocessing.graph import neighbors from .preprocessing.log_scale import log1p, scale from .preprocessing.normalize import normalize_total diff --git a/stlearn/spatials/morphology/adjust.py b/stlearn/spatials/morphology/adjust.py index 2c68bcc2..a97ec258 100644 --- a/stlearn/spatials/morphology/adjust.py +++ b/stlearn/spatials/morphology/adjust.py @@ -1,19 +1,17 @@ -from typing import Literal - import numpy as np import scipy.spatial as spatial from anndata import AnnData from tqdm import tqdm -_SIMILARITY_MATRIX = Literal["cosine", "euclidean", "pearson", "spearman"] -_METHOD = Literal["mean", "median", "sum"] +from stlearn.types import _METHOD, _SIMILARITY_MATRIX + def adjust( adata: AnnData, use_data: str = "X_pca", radius: float = 50.0, rates: int = 1, - method: _SIMILARITY_MATRIX = "mean", + method: _METHOD = "mean", similarity_matrix: _SIMILARITY_MATRIX = "cosine", copy: bool = False, ) -> AnnData | None: diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index 6eb3f3c2..677941b1 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -4,6 +4,8 @@ import scanpy from anndata import AnnData +from stlearn.types import _METHOD + def pseudotime( adata: AnnData, @@ -13,7 +15,7 @@ def pseudotime( use_rep: str = "X_pca", threshold: float = 0.01, radius: int = 50, - method: str = "mean", + method: _METHOD = "mean", threshold_spots: int = 5, use_sme: bool = False, reverse: bool = False, diff --git a/stlearn/spatials/trajectory/set_root.py b/stlearn/spatials/trajectory/set_root.py index 30cf3ae7..47ff44f5 100644 --- a/stlearn/spatials/trajectory/set_root.py +++ b/stlearn/spatials/trajectory/set_root.py @@ -36,8 +36,10 @@ def set_root(adata: AnnData, use_label: str, cluster: str, use_raw: bool = False # Subset the data based on the chosen cluster available_clusters = tmp_adata.obs[use_label].unique() if str(cluster) not in available_clusters.astype(str): - raise ValueError(f"Cluster '{cluster}' not found in available clusters: " + - "{sorted(available_clusters)}") + raise ValueError( + f"Cluster '{cluster}' not found in available clusters: " + + "{sorted(available_clusters)}" + ) tmp_adata = tmp_adata[ tmp_adata.obs[tmp_adata.obs[use_label] == str(cluster)].index, : diff --git a/stlearn/spatials/trajectory/weight_optimization.py b/stlearn/spatials/trajectory/weight_optimization.py index ff4e7385..7c47405e 100644 --- a/stlearn/spatials/trajectory/weight_optimization.py +++ b/stlearn/spatials/trajectory/weight_optimization.py @@ -25,26 +25,34 @@ def weight_optimizing_global( actual_k = k if use_label and list_clusters: if "sub_cluster_labels" not in adata.obs.columns: - print("Warning: 'sub_cluster_labels' column not found. Using provided " + - "k value.") + print( + "Warning: 'sub_cluster_labels' column not found. Using provided " + + "k value." + ) else: try: filtered_data = adata.obs[adata.obs[use_label].isin(list_clusters)] if len(filtered_data) == 0: - raise ValueError(f"No cells found for clusters {list_clusters} " + - "in column '{use_label}'") + raise ValueError( + f"No cells found for clusters {list_clusters} " + + "in column '{use_label}'" + ) # Minimum 1 cluster, use K or max available sub-clusters n_subclusters = len(filtered_data["sub_cluster_labels"].unique()) actual_k = max(1, min(k, n_subclusters)) if actual_k != k: - print(f"Adjusted k from {k} to {actual_k} based on available " + - "sub-clusters ({n_subclusters})") + print( + f"Adjusted k from {k} to {actual_k} based on available " + + "sub-clusters ({n_subclusters})" + ) except Exception as e: - print(f"Warning: Could not determine sub-cluster count: {e}. " + - "Using provided k value.") + print( + f"Warning: Could not determine sub-cluster count: {e}. " + + "Using provided k value." + ) actual_k = k # Screening PTS graph @@ -59,13 +67,17 @@ def weight_optimizing_global( ) as pbar: for i in range(0, total_iterations): weight = round(i * step, 2) - matrix = global_level(adata, use_label=use_label, - list_clusters=list_clusters, use_rep=use_rep, - n_dims=n_dims, w=weight, return_graph=True, - verbose=False, ) - Gs.append( - nx.to_scipy_sparse_array(matrix) + matrix = global_level( + adata, + use_label=use_label, + list_clusters=list_clusters, + use_rep=use_rep, + n_dims=n_dims, + w=weight, + return_graph=True, + verbose=False, ) + Gs.append(nx.to_scipy_sparse_array(matrix)) j = j + step pbar.update(1) @@ -109,10 +121,9 @@ def weight_optimizing_global( return 0.5 -def weight_optimizing_local(adata: AnnData, - use_label: str = "louvain", - cluster=None, - step=0.01): +def weight_optimizing_local( + adata: AnnData, use_label: str = "louvain", cluster=None, step=0.01 +): # Screening PTS graph print("Screening PTS local graph...") Gs = [] @@ -123,8 +134,14 @@ def weight_optimizing_local(adata: AnnData, bar_format="{l_bar}{bar} [ time left: {remaining} ]", ) as pbar: for i in range(0, int(1 / step + 1)): - matrix = local_level(adata, use_label=use_label, cluster=cluster, - w=round(j, 2), verbose=False, return_matrix=True) + matrix = local_level( + adata, + use_label=use_label, + cluster=cluster, + w=round(j, 2), + verbose=False, + return_matrix=True, + ) Gs.append(matrix) j = j + step pbar.update(1) @@ -169,4 +186,3 @@ def weight_optimizing_local(adata: AnnData, def normalize_data(data): return (data - np.min(data)) / (np.max(data) - np.min(data)) - diff --git a/stlearn/tools/label/label.py b/stlearn/tools/label/label.py index 8e95039a..92a8f1a5 100644 --- a/stlearn/tools/label/label.py +++ b/stlearn/tools/label/label.py @@ -90,18 +90,20 @@ def run_label_transfer( def get_counts(data): """Gets count data from anndata if available.""" # Standard layer has counts # - if data.X is not np.ndarray and np.all(np.mod(data.X[0, :].todense(), 1) == 0): + if not isinstance(data.X, np.ndarray) and np.all( + np.mod(data.X[0, :].todense(), 1) == 0 + ): counts = data.to_df().transpose() - elif data.X is np.ndarray and np.all(np.mod(data.X[0, :], 1) == 0): + elif isinstance(data.X, np.ndarray) and np.all(np.mod(data.X[0, :], 1) == 0): counts = data.to_df().transpose() elif ( - data.X is not np.ndarray + not isinstance(data.X, np.ndarray) and hasattr(data, "raw") and np.all(np.mod(data.raw.X[0, :].todense(), 1) == 0) ): counts = data.raw.to_adata()[data.obs_names, data.var_names].to_df().transpose() elif ( - data.X is np.ndarray + isinstance(data.X, np.ndarray) and hasattr(data, "raw") and np.all(np.mod(data.raw.X[0, :], 1) == 0) ): diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index 13c6ae20..3315d253 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -690,7 +690,7 @@ def run_cci( desc="Counting celltype-celltype interactions per LR and permuting " + f"{n_perms} times.", bar_format="{l_bar}{bar} [ time left: {remaining} ]", - disable=verbose is False, + disable=not verbose, ) as pbar: for i, best_lr in enumerate(best_lrs): ligand, receptor = best_lr.split("_") diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tools/microenv/cci/base.py index 2b668728..7824a526 100644 --- a/stlearn/tools/microenv/cci/base.py +++ b/stlearn/tools/microenv/cci/base.py @@ -193,7 +193,10 @@ def get_spot_lrs( if lr.split("_")[0] in df.columns and lr.split("_")[1] in df.columns ] - lr_cols = [pair.split("_")[int(lr_order is False)] for pair in pairs_wRev] + if lr_order: + lr_cols = [pair.split("_")[0] for pair in pairs_wRev] # Get ligand + else: + lr_cols = [pair.split("_")[1] for pair in pairs_wRev] # Get receptor spot_lrs = df[lr_cols] return spot_lrs diff --git a/stlearn/tools/microenv/cci/base_grouping.py b/stlearn/tools/microenv/cci/base_grouping.py index 24201a71..5e229efe 100644 --- a/stlearn/tools/microenv/cci/base_grouping.py +++ b/stlearn/tools/microenv/cci/base_grouping.py @@ -162,7 +162,7 @@ def hotspot_core( total=len(lrs), desc="Removing background lr scores...", bar_format="{l_bar}{bar}", - disable=verbose is False, + disable=not verbose, ) as pbar: for i, lr_ in enumerate(lrs): lr_score_ = score_copy[i, :] diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index cd137ba1..426512a3 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -119,7 +119,7 @@ def get_similar_genes( ------- similar_genes: np.array Array of strings for gene names. """ - if quantiles is float: + if isinstance(quantiles, float): quantiles = np.array([quantiles]) else: quantiles = np.array(quantiles) @@ -186,7 +186,7 @@ def get_similar_genes_Quantiles( similar_genes: np.array Array of strings for gene names. """ - if quantiles is float: + if isinstance(quantiles, float): quantiles = np.array([quantiles]) else: quantiles = np.array(quantiles) diff --git a/stlearn/tools/microenv/cci/permutation.py b/stlearn/tools/microenv/cci/permutation.py index 60bd9bd3..ad9a5f99 100644 --- a/stlearn/tools/microenv/cci/permutation.py +++ b/stlearn/tools/microenv/cci/permutation.py @@ -93,7 +93,7 @@ def perform_spot_testing( total=lr_scores.shape[1], desc="Generating backgrounds & testing each LR pair...", bar_format="{l_bar}{bar} [ time left: {remaining} ]", - disable=verbose is False, + disable=not verbose, ) as pbar: # Keep track of genes which can be used to gen. rand-pairs. gene_bg_genes: dict[str, np.ndarray] = {} @@ -177,7 +177,7 @@ def perform_spot_testing( lr_summary[sig_lrs_in_spot, 1] += 1 lr_summary[sigpval_lrs_in_spot, 2] += 1 - lr_sig_scores[spot_i, sig_lrs_in_spot is False] = 0 + lr_sig_scores[spot_i, ~sig_lrs_in_spot] = 0 # Ordering the results according to number of significant spots per LR# order = np.argsort(-lr_summary[:, 1]) @@ -527,7 +527,7 @@ def get_stats( pvals = np.zeros((1, len(scores)), dtype=np.float)[0, :] nonzero_score_bool = scores > 0 nonzero_score_indices = np.where(nonzero_score_bool)[0] - zero_score_indices = np.where(nonzero_score_bool is False)[0] + zero_score_indices = np.where(~nonzero_score_bool)[0] pvals[zero_score_indices] = (total_bg - len(background)) / total_bg pvals[nonzero_score_indices] = [ len(np.where(background >= scores[i])[0]) / total_bg diff --git a/stlearn/types.py b/stlearn/types.py new file mode 100644 index 00000000..50fe0869 --- /dev/null +++ b/stlearn/types.py @@ -0,0 +1,6 @@ +from typing import Literal + +_SIMILARITY_MATRIX = Literal["cosine", "euclidean", "pearson", "spearman"] +_METHOD = Literal["mean", "median", "sum"] + +__all__ = ["_SIMILARITY_MATRIX", "_METHOD"] From c0f307462cae6f484320dd62608a9354dfe7bfc1 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 11:41:05 +1000 Subject: [PATCH 094/241] Weird renaming issue. --- stlearn/plotting/cci_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 1e544486..1405d6bb 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -761,7 +761,7 @@ def lr_plot( lr_cmap = "default" # This gets ignored due to setting colours below if lr_colors is None: lr_colors = { - ligand: matplotlib.colors.to_hex("receptor"), + ligand: matplotlib.colors.to_hex("r"), receptor: matplotlib.colors.to_hex("limegreen"), lr: matplotlib.colors.to_hex("b"), "": "#836BC6", # Neutral color in H&E images. From d899a5616d93f9f8401ac6d79876784984d24e0d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 12:33:29 +1000 Subject: [PATCH 095/241] Assume .uns[split_node] is string to list of strings. --- .../plotting/trajectory/pseudotime_plot.py | 22 ++++---- .../trajectory/detect_transition_markers.py | 51 ++++++++++++------- stlearn/spatials/trajectory/pseudotime.py | 3 -- stlearn/spatials/trajectory/set_root.py | 2 +- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index 4ee2a115..d6bfe35f 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -1,3 +1,5 @@ +from typing import List + import matplotlib import networkx as nx import numpy as np @@ -12,7 +14,7 @@ def pseudotime_plot( library_id: str | None = None, use_label: str = "louvain", pseudotime_key: str = "pseudotime_key", - list_clusters: str | list | None = None, + list_clusters: str | List[str] | None = None, cell_alpha: float = 1.0, image_alpha: float = 1.0, edge_alpha: float = 0.8, @@ -87,9 +89,8 @@ def pseudotime_plot( imagecol = adata.obs["imagecol"] imagerow = adata.obs["imagerow"] if list_clusters is None: - list_clusters = np.array(range(0, len(adata.obs[use_label].unique()))).astype( - int - ) + unique_labels = adata.obs[use_label].unique() + list_clusters = [str(i) for i in range(len(unique_labels))] tmp = adata.obs G = _read_graph(adata, "global_graph") @@ -157,13 +158,13 @@ def pseudotime_plot( a.text( y[0], y[1], - get_cluster(str(x), adata.uns["split_node"]), + get_cluster(x, adata.uns["split_node"]), color="white", fontsize=node_size, zorder=100, bbox=dict( facecolor=cmap( - int(get_cluster(str(x), adata.uns["split_node"])) + int(get_cluster(x, adata.uns["split_node"])) / (len(used_colors) - 1) ), boxstyle="circle", @@ -220,13 +221,13 @@ def pseudotime_plot( a.text( y[0], y[1], - get_cluster(str(x), adata.uns["split_node"]), + get_cluster(x, adata.uns["split_node"]), color="black", fontsize=8, zorder=100, bbox=dict( facecolor=cmap( - int(get_cluster(str(x), adata.uns["split_node"])) + int(get_cluster(x, adata.uns["split_node"])) / (len(used_colors) - 1) ), boxstyle="circle", @@ -280,5 +281,6 @@ def get_cluster(search, dictionary): def get_node(node_list, split_node): result = np.array([]) for node in node_list: - result = np.append(result, np.array(split_node[int(node)]).astype(int)) - return result.astype(int) + node_ = split_node[node] + result = np.append(result, np.array(node_).astype(int)) + return result diff --git a/stlearn/spatials/trajectory/detect_transition_markers.py b/stlearn/spatials/trajectory/detect_transition_markers.py index b538d147..86ed81c0 100644 --- a/stlearn/spatials/trajectory/detect_transition_markers.py +++ b/stlearn/spatials/trajectory/detect_transition_markers.py @@ -1,7 +1,9 @@ import warnings +from typing import List import numpy as np import pandas as pd +from anndata import AnnData from scipy.stats import spearmanr from ...utils import _read_graph @@ -10,33 +12,46 @@ def detect_transition_markers_clades( - adata, - clade, - cutoff_spearman=0.4, - cutoff_pvalue=0.05, - screening_genes=None, - use_raw_count=False, + adata: AnnData, + clade: int, + cutoff_spearman: float = 0.4, + cutoff_pvalue: float = 0.05, + screening_genes: None | List[str] = None, + use_raw_count: bool = False, ): """\ Transition markers detection of a clade. Parameters ---------- - adata - Annotated data matrix. - clade - Name of a clade user wants to detect transition markers. - cutoff_spearman - The threshold of correlation coefficient. - cutoff_pvalue - The threshold of p-value. - screening_genes - List of customised genes. - use_raw_count + adata : AnnData + Annotated data matrix containing spatial transcriptomics data with + computed pseudotime and clade information. + clade : int + Numeric identifier of the clade for which to detect transition markers. + Should correspond to a clade ID present in the trajectory analysis. + cutoff_spearman : float, default 0.4 + The minimum Spearman correlation coefficient threshold for identifying + significant gene-pseudotime correlations. Must be between 0 and 1. + cutoff_pvalue : float, default 0.05 + The maximum p-value threshold for statistical significance testing. + Must be between 0 and 1. Lower values result in more stringent + statistical filtering. + screening_genes : list of str, optional + Custom list of gene names to restrict the analysis to. If None, + all genes in the dataset will be considered. Useful for focusing + on specific gene sets or reducing computational time. + use_raw_count : bool, default False True if user wants to use raw layer data. Returns ------- - Anndata + AnnData + The input AnnData object with additional information stored in + adata.uns about the detected transition markers, including: + - Correlation coefficients + - P-values + - Gene rankings + - Clade-specific marker information """ print("Detecting the transition markers of clade_" + str(clade) + "...") diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index 677941b1..b2641791 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -99,14 +99,11 @@ def pseudotime( cnt_matrix = adata.uns["paga"]["connectivities"].toarray() # Filter by threshold - cnt_matrix[cnt_matrix < threshold] = 0.0 cnt_matrix = pd.DataFrame(cnt_matrix) # Mapping louvain label to subcluster - cat_ind = adata.uns[use_label + "_index_dict"] - split_node = {} for label in adata.obs[use_label].unique(): meaningful_sub = [] diff --git a/stlearn/spatials/trajectory/set_root.py b/stlearn/spatials/trajectory/set_root.py index 47ff44f5..7c9ce806 100644 --- a/stlearn/spatials/trajectory/set_root.py +++ b/stlearn/spatials/trajectory/set_root.py @@ -16,7 +16,7 @@ def set_root(adata: AnnData, use_label: str, cluster: str, use_raw: bool = False Use label result of cluster method. cluster: str Cluster identifier to use as the root cluster. Must exist in - `adata.obs[use_label]`. Will be converted to string for comparison. + `adata.obs[use_label]`. use_raw: bool, default False If True, use `adata.raw.X` for calculations; otherwise use `adata.X`. Returns From 3d4b6d1cdb6740e27268a86eead48b069ce2f35f Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 13:09:06 +1000 Subject: [PATCH 096/241] Fix some typing and use ~ instead of not inside selector. --- stlearn/plotting/cci_plot.py | 404 +++++++++--------- .../plotting/trajectory/pseudotime_plot.py | 88 ++-- .../trajectory/detect_transition_markers.py | 3 +- 3 files changed, 250 insertions(+), 245 deletions(-) diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 1405d6bb..33917421 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -43,14 +43,14 @@ def lr_diagnostics( - adata, - highlight_lrs: list | None = None, - n_top: int | None = None, - color0: str = "turquoise", - color1: str = "plum", - figsize: tuple = (10, 4), - lr_text_fp: dict | None = None, - show: bool = True, + adata, + highlight_lrs: list | None = None, + n_top: int | None = None, + color0: str = "turquoise", + color1: str = "plum", + figsize: tuple = (10, 4), + lr_text_fp: dict | None = None, + show: bool = True, ): """Diagnostic plot looking at relationship between technical features of lrs and lr rank. Two plots generated: left is the average of the median for nonzero @@ -108,17 +108,17 @@ def lr_diagnostics( def lr_summary( - adata, - n_top: int = 50, - highlight_lrs: list | None = None, - y: str = "n_spots_sig", - color: str = "gold", - figsize: tuple | None = None, - highlight_color: str = "red", - max_text: int = 50, - lr_text_fp: dict | None = None, - ax: plt_axis.Axes | None = None, - show: bool = True, + adata, + n_top: int = 50, + highlight_lrs: list | None = None, + y: str = "n_spots_sig", + color: str = "gold", + figsize: tuple | None = None, + highlight_color: str = "red", + max_text: int = 50, + lr_text_fp: dict | None = None, + ax: plt_axis.Axes | None = None, + show: bool = True, ): """Plotting the top LRs ranked by number of significant spots. @@ -176,17 +176,17 @@ def lr_summary( def lr_n_spots( - adata, - n_top: int = 100, - font_dict: dict | None = None, - xtick_dict: dict | None = None, - bar_width: float = 1, - max_text: int = 50, - non_sig_color: str = "dodgerblue", - sig_color: str = "springgreen", - figsize: tuple = (6, 4), - show_title: bool = True, - show: bool = True, + adata, + n_top: int = 100, + font_dict: dict | None = None, + xtick_dict: dict | None = None, + bar_width: float = 1, + max_text: int = 50, + non_sig_color: str = "dodgerblue", + sig_color: str = "springgreen", + figsize: tuple = (6, 4), + show_title: bool = True, + show: bool = True, ): """Bar plot showing for each LR no. of sig versus non-sig spots. @@ -258,15 +258,15 @@ def lr_n_spots( def lr_go( - adata, - n_top: int = 20, - highlight_go: list | None = None, - figsize=(6, 4), - rot: float = 50, - lr_text_fp: dict | None = None, - highlight_color: str = "yellow", - max_text: int = 50, - show: bool = True, + adata, + n_top: int = 20, + highlight_go: list | None = None, + figsize=(6, 4), + rot: float = 50, + lr_text_fp: dict | None = None, + highlight_color: str = "yellow", + max_text: int = 50, + show: bool = True, ): """Plots the results from the LR GO analysis. @@ -322,13 +322,13 @@ def lr_go( def cci_check( - adata: AnnData, - use_label: str, - figsize=(16, 10), - cell_label_size=20, - axis_text_size=18, - tick_size=14, - show=True, + adata: AnnData, + use_label: str, + figsize=(16, 10), + cell_label_size=20, + axis_text_size=18, + tick_size=14, + show=True, ): """Checks relationship between no. of significant CCI-LR interactions and cell type frequency. @@ -420,32 +420,32 @@ def cci_check( # Functions for visualisation the LR results per spot. def lr_result_plot( - adata: AnnData, - use_lr: Optional["str"] = None, - use_result: Optional["str"] = "lr_sig_scores", - # plotting param - title: str | None = None, - figsize: tuple[float, float] | None = None, - cmap: str = "Spectral_r", - ax: plt_axis.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - zoom_coord: tuple[float, float, float, float] | None = None, - crop: bool = True, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, + adata: AnnData, + use_lr: Optional["str"] = None, + use_result: Optional["str"] = "lr_sig_scores", + # plotting param + title: str | None = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + ax: plt_axis.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + zoom_coord: tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, ): """Plots the per spot statistics for given LR. @@ -538,35 +538,35 @@ def lr_result_plot( # @_docs_params(het_plot=doc_lr_plot) def lr_plot( - adata: AnnData, - lr: str, - min_expr: float = 0, - sig_spots=True, - use_label: str | None = None, - outer_mode: str = "continuous", - l_cmap=None, - r_cmap=None, - lr_cmap=None, - inner_cmap=None, - inner_size_prop: float = 0.25, - middle_size_prop: float = 0.5, - outer_size_prop: float = 1, - pt_scale: int = 100, - title="", - show_image: bool = True, - show_arrows: bool = False, - fig_or_none: plt_figure.Figure | None = None, - ax_or_none: plt_axis.Axes | None = None, - arrow_head_width: float = 4, - arrow_width: float = 0.001, - arrow_cmap: str | None = None, - arrow_vmax: float | None = None, - sig_cci: bool = False, - lr_colors: dict | None = None, - figsize: tuple = (6.4, 4.8), - use_mix: bool | None = None, - # plotting params - **kwargs, + adata: AnnData, + lr: str, + min_expr: float = 0, + sig_spots=True, + use_label: str | None = None, + outer_mode: str = "continuous", + l_cmap=None, + r_cmap=None, + lr_cmap=None, + inner_cmap=None, + inner_size_prop: float = 0.25, + middle_size_prop: float = 0.5, + outer_size_prop: float = 1, + pt_scale: int = 100, + title="", + show_image: bool = True, + show_arrows: bool = False, + fig_or_none: plt_figure.Figure | None = None, + ax_or_none: plt_axis.Axes | None = None, + arrow_head_width: float = 4, + arrow_width: float = 0.001, + arrow_cmap: str | None = None, + arrow_vmax: float | None = None, + sig_cci: bool = False, + lr_colors: dict | None = None, + figsize: tuple = (6.4, 4.8), + use_mix: bool | None = None, + # plotting params + **kwargs, ) -> None: """Creates different kinds of spatial visualisations for the LR analysis results. To see combinations of parameters refer to stLearn CCI tutorial. @@ -672,10 +672,10 @@ def lr_plot( # Making sure have run_cci first with respective labelling # if ( - show_arrows - and sig_cci - and use_label - and f"per_lr_cci_{use_label}" not in adata.uns + show_arrows + and sig_cci + and use_label + and f"per_lr_cci_{use_label}" not in adata.uns ): raise Exception( "Cannot subset arrow interactions to significant ccis " @@ -701,16 +701,18 @@ def lr_plot( "to adata.uns matching the use_mix ({use_mix}) key." ) elif ( - use_label is not None and use_label in lr_use_labels and ran_sig and not lr_sig + use_label is not None + and use_label in lr_use_labels + and ran_sig and not lr_sig ): raise Exception( "Since use_label refers to lr stats & ran permutation testing, " "LR needs to be significant to view stats." ) elif ( - use_label is not None - and use_label not in adata.obs.keys() - and use_label not in lr_use_labels + use_label is not None + and use_label not in adata.obs.keys() + and use_label not in lr_use_labels ): raise Exception( f"use_label must be in adata.obs or one of lr stats: {lr_use_labels}." @@ -890,34 +892,34 @@ def lr_plot( #### from old data structure when only test individual LRs. @_docs_params(spatial_base_plot=doc_spatial_base_plot, het_plot=doc_het_plot) def het_plot( - adata: AnnData, - # plotting param - title: str | None = None, - figsize: tuple[float, float] | None = None, - cmap: str = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: plt_axis.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - zoom_coord: tuple[float, float, float, float] | None = None, - crop: bool = True, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - # cci_rank param - use_het: str = "het", - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, + adata: AnnData, + # plotting param + title: str | None = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: plt_axis.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + zoom_coord: tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # cci_rank param + use_het: str = "het", + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, ) -> None: """\ Allows the visualization of significant cell-cell interaction @@ -974,22 +976,22 @@ def het_plot( def ccinet_plot( - adata: AnnData, - use_label: str, - lr: str | None = None, - pos: dict | None = None, - return_pos: bool = False, - cmap: str = "default", - font_size: int = 12, - node_size_exp: int = 1, - node_size_scaler: int = 1, - min_counts: int = 0, - sig_interactions: bool = True, - fig_or_none: plt_figure.Figure | None = None, - ax_or_none: plt_axis.Axes | None = None, - pad=0.25, - title_or_none: str | None = None, - figsize: tuple = (10, 10), + adata: AnnData, + use_label: str, + lr: str | None = None, + pos: dict | None = None, + return_pos: bool = False, + cmap: str = "default", + font_size: int = 12, + node_size_exp: int = 1, + node_size_scaler: int = 1, + min_counts: int = 0, + sig_interactions: bool = True, + fig_or_none: plt_figure.Figure | None = None, + ax_or_none: plt_axis.Axes | None = None, + pad=0.25, + title_or_none: str | None = None, + figsize: tuple = (10, 10), ): """Circular celltype-celltype interaction network based on LR-CCI analysis. The size of the nodes drawn for each cell type indicates the total no. of @@ -1071,9 +1073,10 @@ def ccinet_plot( node_sizes = np.array( [ ( - ((sum(int_matrix[i, :] + int_matrix[:, i]) - int_matrix[i, i]) / total) - * 10000 - * node_size_scaler + ((sum(int_matrix[i, :] + int_matrix[:, i]) - int_matrix[ + i, i]) / total) + * 10000 + * node_size_scaler ) ** (node_size_exp) for i in node_indices @@ -1087,8 +1090,8 @@ def ccinet_plot( trans_i = np.where(all_set == edge[0][0])[0][0] receive_i = np.where(all_set == edge[0][1])[0][0] e_total = ( - sum(list(int_matrix[trans_i, :]) + list(int_matrix[:, receive_i])) - - int_matrix[trans_i, receive_i] + sum(list(int_matrix[trans_i, :]) + list(int_matrix[:, receive_i])) + - int_matrix[trans_i, receive_i] ) # so don't double count e_totals.append(e_total) edge_weights = [edge[1]["weight"] / e_totals[i] for i, edge in enumerate(edges)] @@ -1157,15 +1160,15 @@ def ccinet_plot( def cci_map( - adata: AnnData, - use_label: str, - lr_or_none: str | None = None, - ax_or_none: plt_axis.Axes | None = None, - show: bool = False, - figsize_or_none: tuple | None = None, - cmap: str = "Spectral_r", - sig_interactions: bool = True, - title=None, + adata: AnnData, + use_label: str, + lr_or_none: str | None = None, + ax_or_none: plt_axis.Axes | None = None, + show: bool = False, + figsize_or_none: tuple | None = None, + cmap: str = "Spectral_r", + sig_interactions: bool = True, + title=None, ): """Heatmap visualising sender->receivers of cell type interactions. @@ -1239,18 +1242,18 @@ def cci_map( def lr_cci_map( - adata: AnnData, - use_label: str, - lrs: list | np.ndarray | None = None, - n_top_lrs: int = 5, - n_top_ccis: int = 15, - min_total: int = 0, - ax_or_none: plt_axis.Axes | None = None, - figsize: tuple = (6.48, 4.8), - show: bool = False, - cmap: str = "Spectral_r", - square_scaler: int = 700, - sig_interactions: bool = True, + adata: AnnData, + use_label: str, + lrs: list | np.ndarray | None = None, + n_top_lrs: int = 5, + n_top_ccis: int = 15, + min_total: int = 0, + ax_or_none: plt_axis.Axes | None = None, + figsize: tuple = (6.48, 4.8), + show: bool = False, + cmap: str = "Spectral_r", + square_scaler: int = 700, + sig_interactions: bool = True, ): """Heatmap of interaction counts. Rows are lrs and columns are celltype->celltype interactions. @@ -1357,18 +1360,18 @@ def lr_cci_map( def lr_chord_plot( - adata: AnnData, - use_label: str, - lr: str | None = None, - min_ints: int = 2, - n_top_ccis: int = 10, - cmap: str = "default", - sig_interactions: bool = True, - label_size: int = 10, - label_rotation: float = 0, - title: str = "", - figsize: tuple = (8, 8), - show: bool = True, + adata: AnnData, + use_label: str, + lr: str | None = None, + min_ints: int = 2, + n_top_ccis: int = 10, + cmap: str = "default", + sig_interactions: bool = True, + label_size: int = 10, + label_rotation: float = 0, + title: str = "", + figsize: tuple = (8, 8), + show: bool = True, ): """Chord diagram of interactions between cell types. Note that interaction is measured as the total no. of edges connecting @@ -1440,7 +1443,7 @@ def lr_chord_plot( all_zero = np.array( [np.all(np.logical_and(flux[i, keep] == 0, flux[keep, i] == 0)) for i in keep] ) - keep = keep[not all_zero] + keep = keep[~all_zero] if len(keep) == 0: # If we don't keep anything, warn the user print( f"Warning: for {lr} at the current min_ints ({min_ints}), there " @@ -1476,7 +1479,7 @@ def lr_chord_plot( rotation = nodePos[i][2] # Prevent text going upside down at certain rotations if (rotation < 90 and rotation > 18 and label_rotation != 0) or ( - rotation < 120 and rotation > 90 + rotation < 120 and rotation > 90 ): label_rotation_ = -label_rotation else: @@ -1492,13 +1495,13 @@ def lr_chord_plot( def grid_plot( - adata, - use_label: str | None = None, - n_row: int = 10, - n_col: int = 10, - size: int = 1, - figsize=(4.5, 4.5), - show: bool = False, + adata, + use_label: str | None = None, + n_row: int = 10, + n_col: int = 10, + size: int = 1, + figsize=(4.5, 4.5), + show: bool = False, ): """Plots grid over the top of spatial data to show how cells will be grouped if gridded. @@ -1576,7 +1579,6 @@ def spatialcci_plot_interactive(adata: AnnData): output_notebook() show(bokeh_object.app, notebook_handle=True) - # def het_plot_interactive(adata: AnnData): # bokeh_object = BokehCciPlot(adata) # output_notebook() diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index d6bfe35f..6ce2af9a 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -1,40 +1,39 @@ -from typing import List - import matplotlib import networkx as nx import numpy as np from anndata import AnnData from matplotlib import pyplot as plt +from numpy._typing import NDArray from stlearn.utils import _read_graph def pseudotime_plot( - adata: AnnData, - library_id: str | None = None, - use_label: str = "louvain", - pseudotime_key: str = "pseudotime_key", - list_clusters: str | List[str] | None = None, - cell_alpha: float = 1.0, - image_alpha: float = 1.0, - edge_alpha: float = 0.8, - node_alpha: float = 1.0, - spot_size: float | int = 6.5, - node_size: float = 5, - show_color_bar: bool = True, - show_axis: bool = False, - show_graph: bool = True, - show_trajectories: bool = False, - reverse: bool = False, - show_node: bool = True, - show_plot: bool = True, - cropped: bool = True, - margin: int = 100, - dpi: int = 150, - output: str | None = None, - name: str | None = None, - copy: bool = False, - ax=None, + adata: AnnData, + library_id: str | None = None, + use_label: str = "louvain", + pseudotime_key: str = "pseudotime_key", + list_clusters: str | list[str] | None = None, + cell_alpha: float = 1.0, + image_alpha: float = 1.0, + edge_alpha: float = 0.8, + node_alpha: float = 1.0, + spot_size: float | int = 6.5, + node_size: float = 5, + show_color_bar: bool = True, + show_axis: bool = False, + show_graph: bool = True, + show_trajectories: bool = False, + reverse: bool = False, + show_node: bool = True, + show_plot: bool = True, + cropped: bool = True, + margin: int = 100, + dpi: int = 150, + output: str | None = None, + name: str | None = None, + copy: bool = False, + ax=None, ) -> AnnData | None: """\ Global trajectory inference plot (Only DPT). @@ -86,18 +85,22 @@ def pseudotime_plot( Nothing """ - imagecol = adata.obs["imagecol"] - imagerow = adata.obs["imagerow"] + checked_list_clusters: list[str] if list_clusters is None: unique_labels = adata.obs[use_label].unique() - list_clusters = [str(i) for i in range(len(unique_labels))] + checked_list_clusters = [str(i) for i in range(len(unique_labels))] + elif isinstance(list_clusters, str): + checked_list_clusters = [list_clusters] + + imagecol = adata.obs["imagecol"] + imagerow = adata.obs["imagerow"] tmp = adata.obs G = _read_graph(adata, "global_graph") labels = nx.get_edge_attributes(G, "weight") result = [] - query_node = get_node(list_clusters, adata.uns["split_node"]) + query_node = get_node(checked_list_clusters, adata.uns["split_node"]) for edge in G.edges(query_node): if (edge[0] in query_node) and (edge[1] in query_node): result.append(edge) @@ -112,7 +115,7 @@ def pseudotime_plot( fig, a = plt.subplots() if ax is not None: a = ax - centroid_dict = adata.uns["centroid_dict"] + centroid_dict: dict[int, NDArray[np.float64]] = adata.uns["centroid_dict"] centroid_dict = {int(key): centroid_dict[key] for key in centroid_dict} dpt = adata.obs[pseudotime_key] vmin = min(dpt) @@ -154,7 +157,7 @@ def pseudotime_plot( ) for x, y in centroid_dict.items(): - if x in get_node(list_clusters, adata.uns["split_node"]): + if x in get_node(checked_list_clusters, adata.uns["split_node"]): a.text( y[0], y[1], @@ -217,7 +220,7 @@ def pseudotime_plot( if show_node: for x, y in centroid_dict.items(): - if x in get_node(list_clusters, adata.uns["split_node"]): + if x in get_node(checked_list_clusters, adata.uns["split_node"]): a.text( y[0], y[1], @@ -272,15 +275,16 @@ def pseudotime_plot( # get name of cluster by subcluster -def get_cluster(search, dictionary): - for cl, sub in dictionary.items(): - if search in sub: +def get_cluster(search: int, split_node: dict[str, list[str]]): + for cl, sub in split_node.items(): + if str(search) in sub: return cl -def get_node(node_list, split_node): - result = np.array([]) +def get_node( + node_list: list[str], split_node: dict[str, list[str]] +) -> NDArray[np.int64]: + all_values = [] for node in node_list: - node_ = split_node[node] - result = np.append(result, np.array(node_).astype(int)) - return result + all_values.extend(split_node[node]) + return np.array([int(val) for val in all_values], dtype=np.int64) diff --git a/stlearn/spatials/trajectory/detect_transition_markers.py b/stlearn/spatials/trajectory/detect_transition_markers.py index 86ed81c0..d41d493e 100644 --- a/stlearn/spatials/trajectory/detect_transition_markers.py +++ b/stlearn/spatials/trajectory/detect_transition_markers.py @@ -1,5 +1,4 @@ import warnings -from typing import List import numpy as np import pandas as pd @@ -16,7 +15,7 @@ def detect_transition_markers_clades( clade: int, cutoff_spearman: float = 0.4, cutoff_pvalue: float = 0.05, - screening_genes: None | List[str] = None, + screening_genes: None | list[str] = None, use_raw_count: bool = False, ): """\ From 39e4d3caafb0632ba50f63337ac8074dbf6620e7 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 13:10:46 +1000 Subject: [PATCH 097/241] Oops. --- stlearn/plotting/trajectory/pseudotime_plot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index 6ce2af9a..1cc59c01 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -91,6 +91,8 @@ def pseudotime_plot( checked_list_clusters = [str(i) for i in range(len(unique_labels))] elif isinstance(list_clusters, str): checked_list_clusters = [list_clusters] + else: + checked_list_clusters = list_clusters imagecol = adata.obs["imagecol"] imagerow = adata.obs["imagerow"] From 5196a3e6a4fd338b37553f71e2f3642f6e7825f4 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 15:30:27 +1000 Subject: [PATCH 098/241] Update copyright. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 272a059b..d83827a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,7 +76,7 @@ # General information about the project. project = "stLearn" -copyright = "2022, Genomics and Machine Learning lab" +copyright = "2022-2025, Genomics and Machine Learning lab" author = "Genomics and Machine Learning lab" # The version info for the project you're documenting, acts as replacement From 697a12a1146b68ead56a8e2e8ec55c71ed63a10b Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 2 Jul 2025 16:58:12 +1000 Subject: [PATCH 099/241] Update versions. --- HISTORY.rst | 14 +++++++++++++- docs/index.rst | 2 ++ docs/installation.rst | 31 ++++++------------------------- docs/release_notes/1.1.0.rst | 15 +++++++++++++++ pyproject.toml | 2 +- stlearn/__init__.py | 2 +- 6 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 docs/release_notes/1.1.0.rst diff --git a/HISTORY.rst b/HISTORY.rst index 5ec9e16e..e1752d99 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -0.5.0 (2025-07-01) +1.1.0 (2025-07-02) ------------------ * Support Python 3.10.x * Added quality checks black, ruff and mypy and fixed appropriate source code. @@ -12,25 +12,37 @@ History API and Bug Fixes: * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. +* pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. 0.4.11 (2022-11-25) ------------------ + 0.4.10 (2022-11-22) ------------------ + 0.4.8 (2022-06-15) ------------------ + 0.4.7 (2022-03-28) ------------------ + 0.4.6 (2022-03-09) ------------------ + 0.4.5 (2022-03-02) ------------------ + 0.4.0 (2022-02-03) ------------------ + 0.3.2 (2021-03-29) ------------------ + 0.3.1 (2020-12-24) ------------------ + 0.2.7 (2020-09-12) ------------------ + 0.2.6 (2020-08-04) +------------------ diff --git a/docs/index.rst b/docs/index.rst index c8e2630c..2d3df2e0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,8 @@ In the new release, we provide the interactive plots: Latest additions ---------------- +.. include:: release_notes/1.1.0.rst + .. include:: release_notes/0.4.11.rst .. include:: release_notes/0.4.6.rst diff --git a/docs/installation.rst b/docs/installation.rst index 26ac7387..b27a8f3a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -13,15 +13,15 @@ Install by Anaconda Prepare conda environment for stLearn :: - conda create -n stlearn python=3.8 - conda activate stlearn + conda create -n stlearn python=3.10 --y + conda activate stlearn **Step 2:** You can directly install stlearn in the anaconda by: :: - conda install -c conda-forge stlearn + conda install -c conda-forge stlearn Install by PyPi --------------- @@ -31,31 +31,12 @@ Install by PyPi Prepare conda environment for stLearn :: - conda create -n stlearn python=3.8 - conda activate stlearn + conda create -n stlearn python=3.10 --y + conda activate stlearn **Step 2:** Install stlearn using `pip` :: - pip install -U stlearn - - - -Popular bugs ---------------- - -- `DLL load failed while importing utilsextension: The specified module could not be found.` - -You need to uninstall package `tables` and install it again -:: - - pip uninstall tables - conda install pytables - -If conda version does not work, you can access to this site and download the .whl file: `https://www.lfd.uci.edu/~gohlke/pythonlibs/#pytables` - -:: - - pip install tables-3.7.0-cp38-cp38-win_amd64.whl + pip install -U stlearn diff --git a/docs/release_notes/1.1.0.rst b/docs/release_notes/1.1.0.rst new file mode 100644 index 00000000..d8674508 --- /dev/null +++ b/docs/release_notes/1.1.0.rst @@ -0,0 +1,15 @@ +1.1.0 `2025-07-02` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. rubric:: Feature + +* Support Python 3.10.x +* Added quality checks black, ruff and mypy and fixed appropriate source code. +* Copy parameters now work with the same semantics as scanpy. +* Library upgrades for leidenalg, louvain, numba, numpy, scanpy, and tensorflow. + +.. rubric:: Bug fixes + +* Consistent with type annotations - mainly missing None annotations. +* pl.cluster_plot - Does not keep colours from previous runs when clustering. +* pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8478ada2..74d75b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "0.5.0" +version = "1.1.0" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 6fe7d20f..51f1e6f3 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -2,7 +2,7 @@ __author__ = """Genomics and Machine Learning lab""" __email__ = "andrew.newman@uq.edu.au" -__version__ = "0.5.0" +__version__ = "1.1.0" from . import add, datasets, em, pl, pp, spatial, tl, types from ._settings import settings From 3ce46842bd8794ad542db4772de0840ff88c0289 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 10:40:19 +1000 Subject: [PATCH 100/241] Add downloading xenium artifacts. Reformat. --- stlearn/_datasets/_datasets.py | 52 ++- stlearn/plotting/cci_plot.py | 402 +++++++++--------- .../plotting/trajectory/pseudotime_plot.py | 52 +-- 3 files changed, 277 insertions(+), 229 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index a637aed3..56fc93ca 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -1,3 +1,5 @@ +import zipfile as zf + import scanpy as sc from anndata import AnnData @@ -10,10 +12,58 @@ def example_bcba() -> AnnData: Reference: https://support.10xgenomics.com/spatial-gene-expression/datasets/1.1.0/V1_Breast_Cancer_Block_A_Section_1 """ - settings.datasetdir.mkdir(exist_ok=True) + settings.datasetdir.mkdir(parents=True, exist_ok=True) filename = settings.datasetdir / "example_bcba.h5" url = "https://www.dropbox.com/s/u3m2f16mvdom1am/example_bcba.h5ad?dl=1" if not filename.is_file(): sc.readwrite._download(url=url, path=filename) adata = sc.read_h5ad(filename) return adata + + +def xenium_sge( + base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", + image_filename="he_image.ome.tif", + zip_filename="outs.zip", + sample_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", + include_hires_tiff: bool = False, +): + """ + Download and extract Xenium SGE data files. + + Args: + base_url: Base URL for downloads + image_filename: Name of the image file to download + zip_filename: Name of the zip file to download + sample_id: Sample identifier + include_hires_tiff: Whether to download the high-res TIFF image + """ + sample_dir = settings.datasetdir / sample_id + sample_dir.mkdir(parents=True, exist_ok=True) + + files_to_extract = ["cell_feature_matrix.h5", "cells.csv.gz"] + all_sge_files_exist = all( + (sample_dir / sge_file).exists() for sge_file in files_to_extract + ) + + download_filenames = [] + if not all_sge_files_exist: + download_filenames.append(zip_filename) + if include_hires_tiff and not (sample_dir / image_filename).exists(): + download_filenames.append(image_filename) + + for file_name in download_filenames: + file_path = sample_dir / file_name + url = f"{base_url}/{sample_id}/{sample_id}_{file_name}" + if not file_path.is_file(): + sc.readwrite._download(url=url, path=file_path) + + if not all_sge_files_exist: + try: + zip_file_path = sample_dir / zip_filename + with zf.ZipFile(zip_file_path, "r") as zip_ref: + for zip_filename in files_to_extract: + with open(sample_dir / zip_filename, "wb") as file_name: + file_name.write(zip_ref.read(f"outs/{zip_filename}")) + except zf.BadZipFile: + raise ValueError(f"Invalid zip file: {zip_file_path}") diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 33917421..33fa4d84 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -43,14 +43,14 @@ def lr_diagnostics( - adata, - highlight_lrs: list | None = None, - n_top: int | None = None, - color0: str = "turquoise", - color1: str = "plum", - figsize: tuple = (10, 4), - lr_text_fp: dict | None = None, - show: bool = True, + adata, + highlight_lrs: list | None = None, + n_top: int | None = None, + color0: str = "turquoise", + color1: str = "plum", + figsize: tuple = (10, 4), + lr_text_fp: dict | None = None, + show: bool = True, ): """Diagnostic plot looking at relationship between technical features of lrs and lr rank. Two plots generated: left is the average of the median for nonzero @@ -108,17 +108,17 @@ def lr_diagnostics( def lr_summary( - adata, - n_top: int = 50, - highlight_lrs: list | None = None, - y: str = "n_spots_sig", - color: str = "gold", - figsize: tuple | None = None, - highlight_color: str = "red", - max_text: int = 50, - lr_text_fp: dict | None = None, - ax: plt_axis.Axes | None = None, - show: bool = True, + adata, + n_top: int = 50, + highlight_lrs: list | None = None, + y: str = "n_spots_sig", + color: str = "gold", + figsize: tuple | None = None, + highlight_color: str = "red", + max_text: int = 50, + lr_text_fp: dict | None = None, + ax: plt_axis.Axes | None = None, + show: bool = True, ): """Plotting the top LRs ranked by number of significant spots. @@ -176,17 +176,17 @@ def lr_summary( def lr_n_spots( - adata, - n_top: int = 100, - font_dict: dict | None = None, - xtick_dict: dict | None = None, - bar_width: float = 1, - max_text: int = 50, - non_sig_color: str = "dodgerblue", - sig_color: str = "springgreen", - figsize: tuple = (6, 4), - show_title: bool = True, - show: bool = True, + adata, + n_top: int = 100, + font_dict: dict | None = None, + xtick_dict: dict | None = None, + bar_width: float = 1, + max_text: int = 50, + non_sig_color: str = "dodgerblue", + sig_color: str = "springgreen", + figsize: tuple = (6, 4), + show_title: bool = True, + show: bool = True, ): """Bar plot showing for each LR no. of sig versus non-sig spots. @@ -258,15 +258,15 @@ def lr_n_spots( def lr_go( - adata, - n_top: int = 20, - highlight_go: list | None = None, - figsize=(6, 4), - rot: float = 50, - lr_text_fp: dict | None = None, - highlight_color: str = "yellow", - max_text: int = 50, - show: bool = True, + adata, + n_top: int = 20, + highlight_go: list | None = None, + figsize=(6, 4), + rot: float = 50, + lr_text_fp: dict | None = None, + highlight_color: str = "yellow", + max_text: int = 50, + show: bool = True, ): """Plots the results from the LR GO analysis. @@ -322,13 +322,13 @@ def lr_go( def cci_check( - adata: AnnData, - use_label: str, - figsize=(16, 10), - cell_label_size=20, - axis_text_size=18, - tick_size=14, - show=True, + adata: AnnData, + use_label: str, + figsize=(16, 10), + cell_label_size=20, + axis_text_size=18, + tick_size=14, + show=True, ): """Checks relationship between no. of significant CCI-LR interactions and cell type frequency. @@ -420,32 +420,32 @@ def cci_check( # Functions for visualisation the LR results per spot. def lr_result_plot( - adata: AnnData, - use_lr: Optional["str"] = None, - use_result: Optional["str"] = "lr_sig_scores", - # plotting param - title: str | None = None, - figsize: tuple[float, float] | None = None, - cmap: str = "Spectral_r", - ax: plt_axis.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - zoom_coord: tuple[float, float, float, float] | None = None, - crop: bool = True, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, + adata: AnnData, + use_lr: Optional["str"] = None, + use_result: Optional["str"] = "lr_sig_scores", + # plotting param + title: str | None = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + ax: plt_axis.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + zoom_coord: tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, ): """Plots the per spot statistics for given LR. @@ -538,35 +538,35 @@ def lr_result_plot( # @_docs_params(het_plot=doc_lr_plot) def lr_plot( - adata: AnnData, - lr: str, - min_expr: float = 0, - sig_spots=True, - use_label: str | None = None, - outer_mode: str = "continuous", - l_cmap=None, - r_cmap=None, - lr_cmap=None, - inner_cmap=None, - inner_size_prop: float = 0.25, - middle_size_prop: float = 0.5, - outer_size_prop: float = 1, - pt_scale: int = 100, - title="", - show_image: bool = True, - show_arrows: bool = False, - fig_or_none: plt_figure.Figure | None = None, - ax_or_none: plt_axis.Axes | None = None, - arrow_head_width: float = 4, - arrow_width: float = 0.001, - arrow_cmap: str | None = None, - arrow_vmax: float | None = None, - sig_cci: bool = False, - lr_colors: dict | None = None, - figsize: tuple = (6.4, 4.8), - use_mix: bool | None = None, - # plotting params - **kwargs, + adata: AnnData, + lr: str, + min_expr: float = 0, + sig_spots=True, + use_label: str | None = None, + outer_mode: str = "continuous", + l_cmap=None, + r_cmap=None, + lr_cmap=None, + inner_cmap=None, + inner_size_prop: float = 0.25, + middle_size_prop: float = 0.5, + outer_size_prop: float = 1, + pt_scale: int = 100, + title="", + show_image: bool = True, + show_arrows: bool = False, + fig_or_none: plt_figure.Figure | None = None, + ax_or_none: plt_axis.Axes | None = None, + arrow_head_width: float = 4, + arrow_width: float = 0.001, + arrow_cmap: str | None = None, + arrow_vmax: float | None = None, + sig_cci: bool = False, + lr_colors: dict | None = None, + figsize: tuple = (6.4, 4.8), + use_mix: bool | None = None, + # plotting params + **kwargs, ) -> None: """Creates different kinds of spatial visualisations for the LR analysis results. To see combinations of parameters refer to stLearn CCI tutorial. @@ -672,10 +672,10 @@ def lr_plot( # Making sure have run_cci first with respective labelling # if ( - show_arrows - and sig_cci - and use_label - and f"per_lr_cci_{use_label}" not in adata.uns + show_arrows + and sig_cci + and use_label + and f"per_lr_cci_{use_label}" not in adata.uns ): raise Exception( "Cannot subset arrow interactions to significant ccis " @@ -701,18 +701,16 @@ def lr_plot( "to adata.uns matching the use_mix ({use_mix}) key." ) elif ( - use_label is not None - and use_label in lr_use_labels - and ran_sig and not lr_sig + use_label is not None and use_label in lr_use_labels and ran_sig and not lr_sig ): raise Exception( "Since use_label refers to lr stats & ran permutation testing, " "LR needs to be significant to view stats." ) elif ( - use_label is not None - and use_label not in adata.obs.keys() - and use_label not in lr_use_labels + use_label is not None + and use_label not in adata.obs.keys() + and use_label not in lr_use_labels ): raise Exception( f"use_label must be in adata.obs or one of lr stats: {lr_use_labels}." @@ -892,34 +890,34 @@ def lr_plot( #### from old data structure when only test individual LRs. @_docs_params(spatial_base_plot=doc_spatial_base_plot, het_plot=doc_het_plot) def het_plot( - adata: AnnData, - # plotting param - title: str | None = None, - figsize: tuple[float, float] | None = None, - cmap: str = "Spectral_r", - use_label: str | None = None, - list_clusters: list | None = None, - ax: plt_axis.Axes | None = None, - fig: matplotlib.figure.Figure | None = None, - show_plot: bool = True, - show_axis: bool = False, - show_image: bool = True, - show_color_bar: bool = True, - zoom_coord: tuple[float, float, float, float] | None = None, - crop: bool = True, - margin: float = 100, - size: float = 7, - image_alpha: float = 1.0, - cell_alpha: float = 1.0, - use_raw: bool = False, - fname: str | None = None, - dpi: int = 120, - # cci_rank param - use_het: str = "het", - contour: bool = False, - step_size: int | None = None, - vmin: float | None = None, - vmax: float | None = None, + adata: AnnData, + # plotting param + title: str | None = None, + figsize: tuple[float, float] | None = None, + cmap: str = "Spectral_r", + use_label: str | None = None, + list_clusters: list | None = None, + ax: plt_axis.Axes | None = None, + fig: matplotlib.figure.Figure | None = None, + show_plot: bool = True, + show_axis: bool = False, + show_image: bool = True, + show_color_bar: bool = True, + zoom_coord: tuple[float, float, float, float] | None = None, + crop: bool = True, + margin: float = 100, + size: float = 7, + image_alpha: float = 1.0, + cell_alpha: float = 1.0, + use_raw: bool = False, + fname: str | None = None, + dpi: int = 120, + # cci_rank param + use_het: str = "het", + contour: bool = False, + step_size: int | None = None, + vmin: float | None = None, + vmax: float | None = None, ) -> None: """\ Allows the visualization of significant cell-cell interaction @@ -976,22 +974,22 @@ def het_plot( def ccinet_plot( - adata: AnnData, - use_label: str, - lr: str | None = None, - pos: dict | None = None, - return_pos: bool = False, - cmap: str = "default", - font_size: int = 12, - node_size_exp: int = 1, - node_size_scaler: int = 1, - min_counts: int = 0, - sig_interactions: bool = True, - fig_or_none: plt_figure.Figure | None = None, - ax_or_none: plt_axis.Axes | None = None, - pad=0.25, - title_or_none: str | None = None, - figsize: tuple = (10, 10), + adata: AnnData, + use_label: str, + lr: str | None = None, + pos: dict | None = None, + return_pos: bool = False, + cmap: str = "default", + font_size: int = 12, + node_size_exp: int = 1, + node_size_scaler: int = 1, + min_counts: int = 0, + sig_interactions: bool = True, + fig_or_none: plt_figure.Figure | None = None, + ax_or_none: plt_axis.Axes | None = None, + pad=0.25, + title_or_none: str | None = None, + figsize: tuple = (10, 10), ): """Circular celltype-celltype interaction network based on LR-CCI analysis. The size of the nodes drawn for each cell type indicates the total no. of @@ -1073,10 +1071,9 @@ def ccinet_plot( node_sizes = np.array( [ ( - ((sum(int_matrix[i, :] + int_matrix[:, i]) - int_matrix[ - i, i]) / total) - * 10000 - * node_size_scaler + ((sum(int_matrix[i, :] + int_matrix[:, i]) - int_matrix[i, i]) / total) + * 10000 + * node_size_scaler ) ** (node_size_exp) for i in node_indices @@ -1090,8 +1087,8 @@ def ccinet_plot( trans_i = np.where(all_set == edge[0][0])[0][0] receive_i = np.where(all_set == edge[0][1])[0][0] e_total = ( - sum(list(int_matrix[trans_i, :]) + list(int_matrix[:, receive_i])) - - int_matrix[trans_i, receive_i] + sum(list(int_matrix[trans_i, :]) + list(int_matrix[:, receive_i])) + - int_matrix[trans_i, receive_i] ) # so don't double count e_totals.append(e_total) edge_weights = [edge[1]["weight"] / e_totals[i] for i, edge in enumerate(edges)] @@ -1160,15 +1157,15 @@ def ccinet_plot( def cci_map( - adata: AnnData, - use_label: str, - lr_or_none: str | None = None, - ax_or_none: plt_axis.Axes | None = None, - show: bool = False, - figsize_or_none: tuple | None = None, - cmap: str = "Spectral_r", - sig_interactions: bool = True, - title=None, + adata: AnnData, + use_label: str, + lr_or_none: str | None = None, + ax_or_none: plt_axis.Axes | None = None, + show: bool = False, + figsize_or_none: tuple | None = None, + cmap: str = "Spectral_r", + sig_interactions: bool = True, + title=None, ): """Heatmap visualising sender->receivers of cell type interactions. @@ -1242,18 +1239,18 @@ def cci_map( def lr_cci_map( - adata: AnnData, - use_label: str, - lrs: list | np.ndarray | None = None, - n_top_lrs: int = 5, - n_top_ccis: int = 15, - min_total: int = 0, - ax_or_none: plt_axis.Axes | None = None, - figsize: tuple = (6.48, 4.8), - show: bool = False, - cmap: str = "Spectral_r", - square_scaler: int = 700, - sig_interactions: bool = True, + adata: AnnData, + use_label: str, + lrs: list | np.ndarray | None = None, + n_top_lrs: int = 5, + n_top_ccis: int = 15, + min_total: int = 0, + ax_or_none: plt_axis.Axes | None = None, + figsize: tuple = (6.48, 4.8), + show: bool = False, + cmap: str = "Spectral_r", + square_scaler: int = 700, + sig_interactions: bool = True, ): """Heatmap of interaction counts. Rows are lrs and columns are celltype->celltype interactions. @@ -1360,18 +1357,18 @@ def lr_cci_map( def lr_chord_plot( - adata: AnnData, - use_label: str, - lr: str | None = None, - min_ints: int = 2, - n_top_ccis: int = 10, - cmap: str = "default", - sig_interactions: bool = True, - label_size: int = 10, - label_rotation: float = 0, - title: str = "", - figsize: tuple = (8, 8), - show: bool = True, + adata: AnnData, + use_label: str, + lr: str | None = None, + min_ints: int = 2, + n_top_ccis: int = 10, + cmap: str = "default", + sig_interactions: bool = True, + label_size: int = 10, + label_rotation: float = 0, + title: str = "", + figsize: tuple = (8, 8), + show: bool = True, ): """Chord diagram of interactions between cell types. Note that interaction is measured as the total no. of edges connecting @@ -1479,7 +1476,7 @@ def lr_chord_plot( rotation = nodePos[i][2] # Prevent text going upside down at certain rotations if (rotation < 90 and rotation > 18 and label_rotation != 0) or ( - rotation < 120 and rotation > 90 + rotation < 120 and rotation > 90 ): label_rotation_ = -label_rotation else: @@ -1495,13 +1492,13 @@ def lr_chord_plot( def grid_plot( - adata, - use_label: str | None = None, - n_row: int = 10, - n_col: int = 10, - size: int = 1, - figsize=(4.5, 4.5), - show: bool = False, + adata, + use_label: str | None = None, + n_row: int = 10, + n_col: int = 10, + size: int = 1, + figsize=(4.5, 4.5), + show: bool = False, ): """Plots grid over the top of spatial data to show how cells will be grouped if gridded. @@ -1579,6 +1576,7 @@ def spatialcci_plot_interactive(adata: AnnData): output_notebook() show(bokeh_object.app, notebook_handle=True) + # def het_plot_interactive(adata: AnnData): # bokeh_object = BokehCciPlot(adata) # output_notebook() diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index 1cc59c01..802d0463 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -9,31 +9,31 @@ def pseudotime_plot( - adata: AnnData, - library_id: str | None = None, - use_label: str = "louvain", - pseudotime_key: str = "pseudotime_key", - list_clusters: str | list[str] | None = None, - cell_alpha: float = 1.0, - image_alpha: float = 1.0, - edge_alpha: float = 0.8, - node_alpha: float = 1.0, - spot_size: float | int = 6.5, - node_size: float = 5, - show_color_bar: bool = True, - show_axis: bool = False, - show_graph: bool = True, - show_trajectories: bool = False, - reverse: bool = False, - show_node: bool = True, - show_plot: bool = True, - cropped: bool = True, - margin: int = 100, - dpi: int = 150, - output: str | None = None, - name: str | None = None, - copy: bool = False, - ax=None, + adata: AnnData, + library_id: str | None = None, + use_label: str = "louvain", + pseudotime_key: str = "pseudotime_key", + list_clusters: str | list[str] | None = None, + cell_alpha: float = 1.0, + image_alpha: float = 1.0, + edge_alpha: float = 0.8, + node_alpha: float = 1.0, + spot_size: float | int = 6.5, + node_size: float = 5, + show_color_bar: bool = True, + show_axis: bool = False, + show_graph: bool = True, + show_trajectories: bool = False, + reverse: bool = False, + show_node: bool = True, + show_plot: bool = True, + cropped: bool = True, + margin: int = 100, + dpi: int = 150, + output: str | None = None, + name: str | None = None, + copy: bool = False, + ax=None, ) -> AnnData | None: """\ Global trajectory inference plot (Only DPT). @@ -284,7 +284,7 @@ def get_cluster(search: int, split_node: dict[str, list[str]]): def get_node( - node_list: list[str], split_node: dict[str, list[str]] + node_list: list[str], split_node: dict[str, list[str]] ) -> NDArray[np.int64]: all_values = [] for node in node_list: From cf7cd055296ca7d9690523cb0fc2c0f1af42ad1d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 10:41:46 +1000 Subject: [PATCH 101/241] Expose. --- stlearn/datasets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stlearn/datasets.py b/stlearn/datasets.py index a8c0721e..de92a5f1 100644 --- a/stlearn/datasets.py +++ b/stlearn/datasets.py @@ -1,5 +1,6 @@ -from ._datasets._datasets import example_bcba +from ._datasets._datasets import example_bcba, xenium_sge __all__ = [ "example_bcba", + "xenium_sge" ] From 615febd42f30f1e52358929c359faebea8e4e2b2 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 11:01:46 +1000 Subject: [PATCH 102/241] Fix documentation. --- stlearn/_datasets/_datasets.py | 23 +++--- stlearn/wrapper/read.py | 143 ++++++++++++++++----------------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index 56fc93ca..9be892f8 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -25,45 +25,46 @@ def xenium_sge( base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", image_filename="he_image.ome.tif", zip_filename="outs.zip", - sample_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", + library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", include_hires_tiff: bool = False, ): """ - Download and extract Xenium SGE data files. + Download and extract Xenium SGE data files. Unlike scanpy this current does not + load the data. Data is located in `settings.datasetdir` / `library_id`. Args: base_url: Base URL for downloads image_filename: Name of the image file to download zip_filename: Name of the zip file to download - sample_id: Sample identifier + library_id: Identifier for the library include_hires_tiff: Whether to download the high-res TIFF image """ - sample_dir = settings.datasetdir / sample_id - sample_dir.mkdir(parents=True, exist_ok=True) + library_dir = settings.datasetdir / library_id + library_dir.mkdir(parents=True, exist_ok=True) files_to_extract = ["cell_feature_matrix.h5", "cells.csv.gz"] all_sge_files_exist = all( - (sample_dir / sge_file).exists() for sge_file in files_to_extract + (library_dir / sge_file).exists() for sge_file in files_to_extract ) download_filenames = [] if not all_sge_files_exist: download_filenames.append(zip_filename) - if include_hires_tiff and not (sample_dir / image_filename).exists(): + if include_hires_tiff and not (library_dir / image_filename).exists(): download_filenames.append(image_filename) for file_name in download_filenames: - file_path = sample_dir / file_name - url = f"{base_url}/{sample_id}/{sample_id}_{file_name}" + file_path = library_dir / file_name + url = f"{base_url}/{library_id}/{library_id}_{file_name}" if not file_path.is_file(): sc.readwrite._download(url=url, path=file_path) if not all_sge_files_exist: try: - zip_file_path = sample_dir / zip_filename + zip_file_path = library_dir / zip_filename with zf.ZipFile(zip_file_path, "r") as zip_ref: for zip_filename in files_to_extract: - with open(sample_dir / zip_filename, "wb") as file_name: + with open(library_dir / zip_filename, "wb") as file_name: file_name.write(zip_ref.read(f"outs/{zip_filename}")) except zf.BadZipFile: raise ValueError(f"Invalid zip file: {zip_file_path}") diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 730666c2..4d21802b 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -11,9 +11,9 @@ import numpy as np import pandas as pd import scanpy +from PIL import Image from anndata import AnnData from matplotlib.image import imread -from PIL import Image import stlearn @@ -22,21 +22,20 @@ def Read10X( - path: str | Path, - genome: str | None = None, - count_file: str = "filtered_feature_bc_matrix.h5", - library_id: str | None = None, - load_images: bool = True, - quality: _Quality = "hires", - image_path: str | Path | None = None, + path: str | Path, + genome: str | None = None, + count_file: str = "filtered_feature_bc_matrix.h5", + library_id: str | None = None, + load_images: bool = True, + quality: _Quality = "hires", + image_path: str | Path | None = None, ) -> AnnData: """\ - Read Visium data from 10X (wrap read_visium from scanpy) + Read data from 10X. - In addition to reading regular 10x output, - this looks for the `spatial` folder and loads images, - coordinates and scale factors. - Based on the `Space Ranger output docs`_. + In addition to reading regular 10x output, this looks for the `spatial` folder + and loads images, coordinates and scale factors. Based on the + `Space Ranger output docs`_. _Space Ranger output docs: https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview @@ -44,14 +43,14 @@ def Read10X( Parameters ---------- path - The path to directory for Visium datafiles. + The path to directory for the datafiles. genome Filter expression to genes within this genome. count_file Which file in the directory to use as the count file. Typically, it would be one of: 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. library_id - Identifier for the Visium library. Can be modified when concatenating multiple + Identifier for the library. Can be modified when concatenating multiple adata objects. load_images Load image or not. @@ -187,7 +186,7 @@ def Read10X( else: scale = adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] + ] image_coor = adata.obsm["spatial"] * scale adata.obs["imagecol"] = image_coor[:, 0] @@ -204,13 +203,13 @@ def Read10X( def ReadOldST( - count_matrix_file: PathLike[str] | str | Iterator[str], - spatial_file: int | str | bytes | PathLike[str] | PathLike[bytes], - image_file: str | Path | None = None, - library_id: str = "OldST", - scale: float = 1.0, - quality: str = "hires", - spot_diameter_fullres: float = 50, + count_matrix_file: PathLike[str] | str | Iterator[str], + spatial_file: int | str | bytes | PathLike[str] | PathLike[bytes], + image_file: str | Path | None = None, + library_id: str = "OldST", + scale: float = 1.0, + quality: str = "hires", + spot_diameter_fullres: float = 50, ) -> AnnData: """\ Read Old Spatial Transcriptomics data @@ -224,7 +223,7 @@ def ReadOldST( image_file Path to the tissue image file library_id - Identifier for the Visium library. Can be modified when concatenating multiple + Identifier for the library. Can be modified when concatenating multiple adata objects. scale Set scale factor. @@ -254,13 +253,13 @@ def ReadOldST( def ReadSlideSeq( - count_matrix_file: str | Path, - spatial_file: str | Path, - library_id: str | None = None, - scale: float | None = None, - quality: str = "hires", - spot_diameter_fullres: float = 50, - background_color: _Background = "white", + count_matrix_file: str | Path, + spatial_file: str | Path, + library_id: str | None = None, + scale: float | None = None, + quality: str = "hires", + spot_diameter_fullres: float = 50, + background_color: _Background = "white", ) -> AnnData: """\ Read Slide-seq data @@ -272,7 +271,7 @@ def ReadSlideSeq( spatial_file Path to the spatial location file. library_id - Identifier for the Visium library. Can be modified when concatenating + Identifier for the library. Can be modified when concatenating multiple adata objects. scale Set scale factor. @@ -326,7 +325,7 @@ def ReadSlideSeq( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" @@ -337,13 +336,13 @@ def ReadSlideSeq( def ReadMERFISH( - count_matrix_file: str | Path, - spatial_file: str | Path, - library_id: str | None = None, - scale: float | None = None, - quality: str = "hires", - spot_diameter_fullres: float = 50, - background_color: _Background = "white", + count_matrix_file: str | Path, + spatial_file: str | Path, + library_id: str | None = None, + scale: float | None = None, + quality: str = "hires", + spot_diameter_fullres: float = 50, + background_color: _Background = "white", ) -> AnnData: """\ Read MERFISH data @@ -355,7 +354,7 @@ def ReadMERFISH( spatial_file Path to the spatial location file. library_id - Identifier for the Visium library. Can be modified when concatenating + Identifier for the library. Can be modified when concatenating multiple adata objects. scale Set scale factor. @@ -411,7 +410,7 @@ def ReadMERFISH( adata_merfish.uns["spatial"][library_id]["scalefactors"] = {} adata_merfish.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata_merfish.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -420,14 +419,14 @@ def ReadMERFISH( def ReadSeqFish( - count_matrix_file: str | Path, - spatial_file: str | Path, - library_id: str | None = None, - scale: float = 1.0, - quality: str = "hires", - field: int = 0, - spot_diameter_fullres: float = 50, - background_color: _Background = "white", + count_matrix_file: str | Path, + spatial_file: str | Path, + library_id: str | None = None, + scale: float = 1.0, + quality: str = "hires", + field: int = 0, + spot_diameter_fullres: float = 50, + background_color: _Background = "white", ) -> AnnData: """\ Read SeqFish data @@ -439,7 +438,7 @@ def ReadSeqFish( spatial_file Path to spatial location file. library_id - Identifier for the visium library. Can be modified when concatenating multiple + Identifier for the library. Can be modified when concatenating multiple adata objects. scale Set scale factor. @@ -499,7 +498,7 @@ def ReadSeqFish( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -508,14 +507,14 @@ def ReadSeqFish( def ReadXenium( - feature_cell_matrix_file: str | Path, - cell_summary_file: str | Path, - image_path: Path | None = None, - library_id: str | None = None, - scale: float = 1.0, - quality: str = "hires", - spot_diameter_fullres: float = 15, - background_color: _Background = "white", + feature_cell_matrix_file: str | Path, + cell_summary_file: str | Path, + image_path: Path | None = None, + library_id: str | None = None, + scale: float = 1.0, + quality: str = "hires", + spot_diameter_fullres: float = 15, + background_color: _Background = "white", ) -> AnnData: """\ Read Xenium data @@ -529,7 +528,7 @@ def ReadXenium( image_path Path to image. Only need when loading full resolution image. library_id - Identifier for the visium library. Can be modified when concatenating multiple + Identifier for the Xenium library. Can be modified when concatenating multiple adata objects. scale Set scale factor. @@ -592,7 +591,7 @@ def ReadXenium( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -601,14 +600,14 @@ def ReadXenium( def create_stlearn( - count: pd.DataFrame, - spatial: pd.DataFrame, - library_id: str, - image_path: Path | None = None, - scale: float | None = None, - quality: str = "hires", - spot_diameter_fullres: float = 50, - background_color: _Background = "white", + count: pd.DataFrame, + spatial: pd.DataFrame, + library_id: str, + image_path: Path | None = None, + scale: float | None = None, + quality: str = "hires", + spot_diameter_fullres: float = 50, + background_color: _Background = "white", ): """\ Create AnnData object for stLearn @@ -620,7 +619,7 @@ def create_stlearn( spatial Pandas Dataframe of spatial location of cells/spots. library_id - Identifier for the visium library. Can be modified when concatenating multiple + Identifier for the library. Can be modified when concatenating multiple adata objects. scale Set scale factor. @@ -674,7 +673,7 @@ def create_stlearn( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres From f4321322ece628ceb335bbf537f5d4b75eba4989 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 13:07:08 +1000 Subject: [PATCH 103/241] Fix types, read experiment file and apply coordinate translation. --- stlearn/_datasets/_datasets.py | 13 +++---- stlearn/types.py | 9 ++++- stlearn/wrapper/read.py | 53 ++++++++++++++++++++++------- stlearn/wrapper/xenium_alignment.py | 38 +++++++++++++++++++++ 4 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 stlearn/wrapper/xenium_alignment.py diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index 9be892f8..67005541 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -22,11 +22,11 @@ def example_bcba() -> AnnData: def xenium_sge( - base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", - image_filename="he_image.ome.tif", - zip_filename="outs.zip", - library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", - include_hires_tiff: bool = False, + base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", + image_filename="he_image.ome.tif", + zip_filename="outs.zip", + library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", + include_hires_tiff: bool = False, ): """ Download and extract Xenium SGE data files. Unlike scanpy this current does not @@ -42,7 +42,8 @@ def xenium_sge( library_dir = settings.datasetdir / library_id library_dir.mkdir(parents=True, exist_ok=True) - files_to_extract = ["cell_feature_matrix.h5", "cells.csv.gz"] + files_to_extract = ["cell_feature_matrix.h5", "cells.csv.gz", "imagealignment.csv", + "experiment.xenium"] all_sge_files_exist = all( (library_dir / sge_file).exists() for sge_file in files_to_extract ) diff --git a/stlearn/types.py b/stlearn/types.py index 50fe0869..87ad2447 100644 --- a/stlearn/types.py +++ b/stlearn/types.py @@ -2,5 +2,12 @@ _SIMILARITY_MATRIX = Literal["cosine", "euclidean", "pearson", "spearman"] _METHOD = Literal["mean", "median", "sum"] +_QUALITY = Literal["fulres", "hires", "lowres"] +_BACKGROUND = Literal["black", "white"] -__all__ = ["_SIMILARITY_MATRIX", "_METHOD"] +__all__ = [ + "_SIMILARITY_MATRIX", + "_METHOD", + "_QUALITY", + "_BACKGROUND" +] diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 4d21802b..5050504b 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -5,7 +5,6 @@ from collections.abc import Iterator from os import PathLike from pathlib import Path -from typing import Literal import matplotlib.pyplot as plt import numpy as np @@ -16,9 +15,8 @@ from matplotlib.image import imread import stlearn - -_Quality = Literal["fulres", "hires", "lowres"] -_Background = Literal["black", "white"] +from stlearn.types import _QUALITY, _BACKGROUND +from stlearn.wrapper.xenium_alignment import apply_alignment_transformation def Read10X( @@ -27,7 +25,7 @@ def Read10X( count_file: str = "filtered_feature_bc_matrix.h5", library_id: str | None = None, load_images: bool = True, - quality: _Quality = "hires", + quality: _QUALITY = "hires", image_path: str | Path | None = None, ) -> AnnData: """\ @@ -259,7 +257,7 @@ def ReadSlideSeq( scale: float | None = None, quality: str = "hires", spot_diameter_fullres: float = 50, - background_color: _Background = "white", + background_color: _BACKGROUND = "white", ) -> AnnData: """\ Read Slide-seq data @@ -342,7 +340,7 @@ def ReadMERFISH( scale: float | None = None, quality: str = "hires", spot_diameter_fullres: float = 50, - background_color: _Background = "white", + background_color: _BACKGROUND = "white", ) -> AnnData: """\ Read MERFISH data @@ -426,7 +424,7 @@ def ReadSeqFish( quality: str = "hires", field: int = 0, spot_diameter_fullres: float = 50, - background_color: _Background = "white", + background_color: _BACKGROUND = "white", ) -> AnnData: """\ Read SeqFish data @@ -514,7 +512,10 @@ def ReadXenium( scale: float = 1.0, quality: str = "hires", spot_diameter_fullres: float = 15, - background_color: _Background = "white", + background_color: _BACKGROUND = "white", + alignment_matrix_file: str | Path | None = None, + experiment_xenium_file: str | Path | None = None, + default_pixel_size_microns: float = 0.2125, ) -> AnnData: """\ Read Xenium data @@ -539,6 +540,14 @@ def ReadXenium( Diameter of spot in full resolution background_color Color of the background. Only `black` or `white` is allowed. + alignment_matrix_file + Path to transformation matrix CSV file exported from Xenium Explorer. + If provided, coordinates will be transformed according to coordinate_space. + experiment_xenium_file + Path to experiment.xenium JSON file. If provided, pixel_size will be read from + here. + default_pixel_size_microns + Pixel size in microns (default 0.2125 for Xenium data). Returns ------- AnnData @@ -548,9 +557,29 @@ def ReadXenium( adata = scanpy.read_10x_h5(feature_cell_matrix_file) - spatial = metadata[["x_centroid", "y_centroid"]] - spatial.columns = ["imagecol", "imagerow"] + # Get original spatial coordinates + spatial = metadata[["x_centroid", "y_centroid"]].copy() + + # Get pixel size from experiment.xenium file or use parameter + if experiment_xenium_file is not None: + with open(experiment_xenium_file, 'r') as f: + experiment_data = json.load(f) + pixel_size_microns = experiment_data.get('pixel_size') + else: + pixel_size_microns = default_pixel_size_microns + print(f"Warning: Using default pixel size of {pixel_size_microns} microns. " + "Consider providing experiment_xenium_file for accurate pixel size.") + + # Get and apply alignment transformation if provided + if alignment_matrix_file is not None: + transform_mat = pd.read_csv(alignment_matrix_file, header=None).values + spatial = apply_alignment_transformation( + spatial, + transform_mat, + pixel_size_microns, + ) + spatial.columns = ["imagecol", "imagerow"] adata.obsm["spatial"] = spatial.values if scale is None: @@ -607,7 +636,7 @@ def create_stlearn( scale: float | None = None, quality: str = "hires", spot_diameter_fullres: float = 50, - background_color: _Background = "white", + background_color: _BACKGROUND = "white", ): """\ Create AnnData object for stLearn diff --git a/stlearn/wrapper/xenium_alignment.py b/stlearn/wrapper/xenium_alignment.py new file mode 100644 index 00000000..1d045afd --- /dev/null +++ b/stlearn/wrapper/xenium_alignment.py @@ -0,0 +1,38 @@ +from pathlib import Path +import numpy as np +import pandas as pd + +def apply_alignment_transformation( + coordinates: pd.DataFrame, + transform_mat: np.ndarray, + pixel_size_microns: float = 0.2125, +) -> pd.DataFrame: + """ + Apply transformation matrix to convert coordinates between spaces. + + From https://kb.10xgenomics.com/hc/en-us/articles/35386990499853-How-can-I-convert-coordinates-between-H-E-image-and-Xenium-data + + Parameters + ---------- + coordinates + DataFrame with columns ['x_centroid', 'y_centroid'] in microns + transform_mat + Transformation matrix from Xenium project. + pixel_size_microns + Pixel size in microns + + Returns + ------- + pd.DataFrame + Transformed coordinates + """ + + # Microns to pixels and use inverse transformation matrix + coords_pixels = coordinates.values / pixel_size_microns + transform_mat_inv = np.linalg.inv(transform_mat) + coords_homogeneous = np.column_stack([coords_pixels, np.ones(len(coords_pixels))]) + transformed_coords = np.dot(coords_homogeneous, transform_mat_inv.T) + + # Extract x, y coordinates (ignore homogeneous coordinate) + result_coords = transformed_coords[:, :2] + return pd.DataFrame(result_coords, columns=coordinates.columns) From 86806ccdbd07712a011c13666bb49b55b90ebe05 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 13:12:46 +1000 Subject: [PATCH 104/241] Fix. --- stlearn/_datasets/_datasets.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index 67005541..7969d689 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -42,8 +42,7 @@ def xenium_sge( library_dir = settings.datasetdir / library_id library_dir.mkdir(parents=True, exist_ok=True) - files_to_extract = ["cell_feature_matrix.h5", "cells.csv.gz", "imagealignment.csv", - "experiment.xenium"] + files_to_extract = ["cell_feature_matrix.h5", "cells.csv.gz", "experiment.xenium"] all_sge_files_exist = all( (library_dir / sge_file).exists() for sge_file in files_to_extract ) @@ -52,7 +51,7 @@ def xenium_sge( if not all_sge_files_exist: download_filenames.append(zip_filename) if include_hires_tiff and not (library_dir / image_filename).exists(): - download_filenames.append(image_filename) + download_filenames += ["imagealignment.csv", image_filename] for file_name in download_filenames: file_path = library_dir / file_name @@ -68,4 +67,4 @@ def xenium_sge( with open(library_dir / zip_filename, "wb") as file_name: file_name.write(zip_ref.read(f"outs/{zip_filename}")) except zf.BadZipFile: - raise ValueError(f"Invalid zip file: {zip_file_path}") + raise ValueError(f"Invalid zip file: {library_dir / zip_filename}") From 8873c263003baf705f672ef12d93bc6631db8854 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 13:23:50 +1000 Subject: [PATCH 105/241] Fix. --- stlearn/_datasets/_datasets.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index 7969d689..8c7fc0ac 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -24,6 +24,7 @@ def example_bcba() -> AnnData: def xenium_sge( base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", image_filename="he_image.ome.tif", + alignment_filename="imagealignment.csv", zip_filename="outs.zip", library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", include_hires_tiff: bool = False, @@ -35,6 +36,7 @@ def xenium_sge( Args: base_url: Base URL for downloads image_filename: Name of the image file to download + alignment_filename: Name of the affine transformation file to download zip_filename: Name of the zip file to download library_id: Identifier for the library include_hires_tiff: Whether to download the high-res TIFF image @@ -50,8 +52,11 @@ def xenium_sge( download_filenames = [] if not all_sge_files_exist: download_filenames.append(zip_filename) - if include_hires_tiff and not (library_dir / image_filename).exists(): - download_filenames += ["imagealignment.csv", image_filename] + if (include_hires_tiff + and not (library_dir / alignment_filename).exists() + and not (library_dir / image_filename).exists() + ): + download_filenames += [alignment_filename, image_filename] for file_name in download_filenames: file_path = library_dir / file_name From 99e820e1ca321f5d5bd2a4b352d73f833506527e Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 13:31:40 +1000 Subject: [PATCH 106/241] Fix logic and filename. --- stlearn/_datasets/_datasets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index 8c7fc0ac..da9d36e2 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -24,7 +24,7 @@ def example_bcba() -> AnnData: def xenium_sge( base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", image_filename="he_image.ome.tif", - alignment_filename="imagealignment.csv", + alignment_filename="he_imagealignment.csv", zip_filename="outs.zip", library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", include_hires_tiff: bool = False, @@ -53,8 +53,8 @@ def xenium_sge( if not all_sge_files_exist: download_filenames.append(zip_filename) if (include_hires_tiff - and not (library_dir / alignment_filename).exists() - and not (library_dir / image_filename).exists() + and (not (library_dir / alignment_filename).exists() + or not (library_dir / image_filename).exists()) ): download_filenames += [alignment_filename, image_filename] From c5b01606c1fefdb3f9d91c4bc97d36b660f28a20 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 13:36:41 +1000 Subject: [PATCH 107/241] Add to docs. --- docs/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.rst b/docs/api.rst index 19568d0a..324ff0e5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -208,3 +208,4 @@ Tools: `datasets` :toctree: . datasets.example_bcba() + datasets.xenium_sge() From c6413a5f6da82a9645f49b682dc6bcc6635b30e6 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 13:58:31 +1000 Subject: [PATCH 108/241] Update changes. --- HISTORY.rst | 2 ++ docs/release_notes/1.1.0.rst | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index e1752d99..d9fe79b3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,8 +8,10 @@ History * Added quality checks black, ruff and mypy and fixed appropriate source code. * Copy parameters now work with the same semantics as scanpy. * Library upgrades for leidenalg, louvain, numba, numpy, scanpy, and tensorflow. +* .datasets.xenium_sge - loads Xenium data (and caches it) similar to scanpy's visium_sge call. API and Bug Fixes: +* Xenium TIFF and cell positions are now aligned. * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. diff --git a/docs/release_notes/1.1.0.rst b/docs/release_notes/1.1.0.rst index d8674508..a08714b6 100644 --- a/docs/release_notes/1.1.0.rst +++ b/docs/release_notes/1.1.0.rst @@ -1,15 +1,17 @@ 1.1.0 `2025-07-02` ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. rubric:: Feature +.. rubric:: Features * Support Python 3.10.x * Added quality checks black, ruff and mypy and fixed appropriate source code. * Copy parameters now work with the same semantics as scanpy. * Library upgrades for leidenalg, louvain, numba, numpy, scanpy, and tensorflow. +* .datasets.xenium_sge - loads Xenium data (and caches it) similar to scanpy's visium_sge call. .. rubric:: Bug fixes +* Xenium TIFF and cell positions are now aligned. * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. \ No newline at end of file From 37f48237d3c89fd7f31c9cd3f10e0c2bbca2b4c5 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 15:22:48 +1000 Subject: [PATCH 109/241] Fix formatting --- stlearn/_datasets/_datasets.py | 18 ++-- stlearn/datasets.py | 5 +- stlearn/types.py | 7 +- stlearn/wrapper/read.py | 136 ++++++++++++++-------------- stlearn/wrapper/xenium_alignment.py | 2 +- 5 files changed, 81 insertions(+), 87 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index da9d36e2..b7ae9d14 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -22,12 +22,12 @@ def example_bcba() -> AnnData: def xenium_sge( - base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", - image_filename="he_image.ome.tif", - alignment_filename="he_imagealignment.csv", - zip_filename="outs.zip", - library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", - include_hires_tiff: bool = False, + base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", + image_filename="he_image.ome.tif", + alignment_filename="he_imagealignment.csv", + zip_filename="outs.zip", + library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", + include_hires_tiff: bool = False, ): """ Download and extract Xenium SGE data files. Unlike scanpy this current does not @@ -52,9 +52,9 @@ def xenium_sge( download_filenames = [] if not all_sge_files_exist: download_filenames.append(zip_filename) - if (include_hires_tiff - and (not (library_dir / alignment_filename).exists() - or not (library_dir / image_filename).exists()) + if include_hires_tiff and ( + not (library_dir / alignment_filename).exists() + or not (library_dir / image_filename).exists() ): download_filenames += [alignment_filename, image_filename] diff --git a/stlearn/datasets.py b/stlearn/datasets.py index de92a5f1..f5f99e4a 100644 --- a/stlearn/datasets.py +++ b/stlearn/datasets.py @@ -1,6 +1,3 @@ from ._datasets._datasets import example_bcba, xenium_sge -__all__ = [ - "example_bcba", - "xenium_sge" -] +__all__ = ["example_bcba", "xenium_sge"] diff --git a/stlearn/types.py b/stlearn/types.py index 87ad2447..3006b748 100644 --- a/stlearn/types.py +++ b/stlearn/types.py @@ -5,9 +5,4 @@ _QUALITY = Literal["fulres", "hires", "lowres"] _BACKGROUND = Literal["black", "white"] -__all__ = [ - "_SIMILARITY_MATRIX", - "_METHOD", - "_QUALITY", - "_BACKGROUND" -] +__all__ = ["_SIMILARITY_MATRIX", "_METHOD", "_QUALITY", "_BACKGROUND"] diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 5050504b..6e982e67 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -10,23 +10,23 @@ import numpy as np import pandas as pd import scanpy -from PIL import Image from anndata import AnnData from matplotlib.image import imread +from PIL import Image import stlearn -from stlearn.types import _QUALITY, _BACKGROUND +from stlearn.types import _BACKGROUND, _QUALITY from stlearn.wrapper.xenium_alignment import apply_alignment_transformation def Read10X( - path: str | Path, - genome: str | None = None, - count_file: str = "filtered_feature_bc_matrix.h5", - library_id: str | None = None, - load_images: bool = True, - quality: _QUALITY = "hires", - image_path: str | Path | None = None, + path: str | Path, + genome: str | None = None, + count_file: str = "filtered_feature_bc_matrix.h5", + library_id: str | None = None, + load_images: bool = True, + quality: _QUALITY = "hires", + image_path: str | Path | None = None, ) -> AnnData: """\ Read data from 10X. @@ -184,7 +184,7 @@ def Read10X( else: scale = adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] + ] image_coor = adata.obsm["spatial"] * scale adata.obs["imagecol"] = image_coor[:, 0] @@ -201,13 +201,13 @@ def Read10X( def ReadOldST( - count_matrix_file: PathLike[str] | str | Iterator[str], - spatial_file: int | str | bytes | PathLike[str] | PathLike[bytes], - image_file: str | Path | None = None, - library_id: str = "OldST", - scale: float = 1.0, - quality: str = "hires", - spot_diameter_fullres: float = 50, + count_matrix_file: PathLike[str] | str | Iterator[str], + spatial_file: int | str | bytes | PathLike[str] | PathLike[bytes], + image_file: str | Path | None = None, + library_id: str = "OldST", + scale: float = 1.0, + quality: str = "hires", + spot_diameter_fullres: float = 50, ) -> AnnData: """\ Read Old Spatial Transcriptomics data @@ -251,13 +251,13 @@ def ReadOldST( def ReadSlideSeq( - count_matrix_file: str | Path, - spatial_file: str | Path, - library_id: str | None = None, - scale: float | None = None, - quality: str = "hires", - spot_diameter_fullres: float = 50, - background_color: _BACKGROUND = "white", + count_matrix_file: str | Path, + spatial_file: str | Path, + library_id: str | None = None, + scale: float | None = None, + quality: str = "hires", + spot_diameter_fullres: float = 50, + background_color: _BACKGROUND = "white", ) -> AnnData: """\ Read Slide-seq data @@ -323,7 +323,7 @@ def ReadSlideSeq( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" @@ -334,13 +334,13 @@ def ReadSlideSeq( def ReadMERFISH( - count_matrix_file: str | Path, - spatial_file: str | Path, - library_id: str | None = None, - scale: float | None = None, - quality: str = "hires", - spot_diameter_fullres: float = 50, - background_color: _BACKGROUND = "white", + count_matrix_file: str | Path, + spatial_file: str | Path, + library_id: str | None = None, + scale: float | None = None, + quality: str = "hires", + spot_diameter_fullres: float = 50, + background_color: _BACKGROUND = "white", ) -> AnnData: """\ Read MERFISH data @@ -408,7 +408,7 @@ def ReadMERFISH( adata_merfish.uns["spatial"][library_id]["scalefactors"] = {} adata_merfish.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata_merfish.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -417,14 +417,14 @@ def ReadMERFISH( def ReadSeqFish( - count_matrix_file: str | Path, - spatial_file: str | Path, - library_id: str | None = None, - scale: float = 1.0, - quality: str = "hires", - field: int = 0, - spot_diameter_fullres: float = 50, - background_color: _BACKGROUND = "white", + count_matrix_file: str | Path, + spatial_file: str | Path, + library_id: str | None = None, + scale: float = 1.0, + quality: str = "hires", + field: int = 0, + spot_diameter_fullres: float = 50, + background_color: _BACKGROUND = "white", ) -> AnnData: """\ Read SeqFish data @@ -496,7 +496,7 @@ def ReadSeqFish( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -505,17 +505,17 @@ def ReadSeqFish( def ReadXenium( - feature_cell_matrix_file: str | Path, - cell_summary_file: str | Path, - image_path: Path | None = None, - library_id: str | None = None, - scale: float = 1.0, - quality: str = "hires", - spot_diameter_fullres: float = 15, - background_color: _BACKGROUND = "white", - alignment_matrix_file: str | Path | None = None, - experiment_xenium_file: str | Path | None = None, - default_pixel_size_microns: float = 0.2125, + feature_cell_matrix_file: str | Path, + cell_summary_file: str | Path, + image_path: Path | None = None, + library_id: str | None = None, + scale: float = 1.0, + quality: str = "hires", + spot_diameter_fullres: float = 15, + background_color: _BACKGROUND = "white", + alignment_matrix_file: str | Path | None = None, + experiment_xenium_file: str | Path | None = None, + default_pixel_size_microns: float = 0.2125, ) -> AnnData: """\ Read Xenium data @@ -562,13 +562,15 @@ def ReadXenium( # Get pixel size from experiment.xenium file or use parameter if experiment_xenium_file is not None: - with open(experiment_xenium_file, 'r') as f: + with open(experiment_xenium_file) as f: experiment_data = json.load(f) - pixel_size_microns = experiment_data.get('pixel_size') + pixel_size_microns = experiment_data.get("pixel_size") else: pixel_size_microns = default_pixel_size_microns - print(f"Warning: Using default pixel size of {pixel_size_microns} microns. " - "Consider providing experiment_xenium_file for accurate pixel size.") + print( + f"Warning: Using default pixel size of {pixel_size_microns} microns. " + "Consider providing experiment_xenium_file for accurate pixel size." + ) # Get and apply alignment transformation if provided if alignment_matrix_file is not None: @@ -620,7 +622,7 @@ def ReadXenium( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres @@ -629,14 +631,14 @@ def ReadXenium( def create_stlearn( - count: pd.DataFrame, - spatial: pd.DataFrame, - library_id: str, - image_path: Path | None = None, - scale: float | None = None, - quality: str = "hires", - spot_diameter_fullres: float = 50, - background_color: _BACKGROUND = "white", + count: pd.DataFrame, + spatial: pd.DataFrame, + library_id: str, + image_path: Path | None = None, + scale: float | None = None, + quality: str = "hires", + spot_diameter_fullres: float = 50, + background_color: _BACKGROUND = "white", ): """\ Create AnnData object for stLearn @@ -702,7 +704,7 @@ def create_stlearn( adata.uns["spatial"][library_id]["scalefactors"] = {} adata.uns["spatial"][library_id]["scalefactors"][ "tissue_" + quality + "_scalef" - ] = scale + ] = scale adata.uns["spatial"][library_id]["scalefactors"][ "spot_diameter_fullres" ] = spot_diameter_fullres diff --git a/stlearn/wrapper/xenium_alignment.py b/stlearn/wrapper/xenium_alignment.py index 1d045afd..368565db 100644 --- a/stlearn/wrapper/xenium_alignment.py +++ b/stlearn/wrapper/xenium_alignment.py @@ -1,7 +1,7 @@ -from pathlib import Path import numpy as np import pandas as pd + def apply_alignment_transformation( coordinates: pd.DataFrame, transform_mat: np.ndarray, From b95aa129a876946495d17185df8634ee612f4d47 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 3 Jul 2025 15:31:09 +1000 Subject: [PATCH 110/241] Fix parameter. --- stlearn/tools/clustering/kmeans.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/stlearn/tools/clustering/kmeans.py b/stlearn/tools/clustering/kmeans.py index 822130af..e055b15a 100644 --- a/stlearn/tools/clustering/kmeans.py +++ b/stlearn/tools/clustering/kmeans.py @@ -15,7 +15,7 @@ def kmeans( tol: float = 0.0001, random_state: int | np.random.RandomState = None, copy_x: bool = True, - algorithm: str = "auto", + algorithm: str = "lloyd", key_added: str = "kmeans", copy: bool = False, ) -> AnnData | None: @@ -37,7 +37,7 @@ def kmeans( Maximum number of iterations of the k-means algorithm for a single run. tol - Relative tolerance with regards to inertia to declare convergence. + Relative tolerance with regard to inertia to declare convergence. random_state Determines random number generation for centroid initialization. Use an int to make the randomness deterministic. @@ -50,10 +50,9 @@ def kmeans( the data mean, in this case it will also not ensure that data is C-contiguous which may cause a significant slowdown. algorithm - K-means algorithm to use. The classical EM-style algorithm is "full". - The "elkan" variation is more efficient by using the triangle - inequality, but currently doesn't support sparse data. "auto" chooses - "elkan" for dense data and "full" for sparse data. + K-means algorithm to use. The classical EM-style algorithm is "lloyd". + The "elkan" variation can be more efficient on some datasets with + well-defined clusters, by using the triangle inequality. key_added Key add to adata.obs copy From d6a3cfa4dff46a2bdaae0407822a0af37d7af242 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 4 Jul 2025 16:30:38 +1000 Subject: [PATCH 111/241] Set location of colorbar to be center left rather than best. --- stlearn/plotting/classes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 343bdc93..89604f93 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -190,6 +190,7 @@ def _add_color_bar(self, plot, color_bar_label: str = ""): shrink=0.5, cmap=self.cmap, label=color_bar_label, + loc="center left" ) cb.outline.set_visible(False) From 5a30d3150e18056c270cf581938c9ff0604eaa2c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 10:27:07 +1000 Subject: [PATCH 112/241] Use scanpy to get data instead of one bc sample in dropbox. --- docs/api.rst | 2 +- stlearn/_datasets/_datasets.py | 36 ++++++++++++++++++----------- stlearn/datasets.py | 4 ++-- stlearn/plotting/cci_plot.py | 2 +- stlearn/plotting/cluster_plot.py | 2 +- stlearn/plotting/feat_plot.py | 2 +- stlearn/plotting/gene_plot.py | 2 +- stlearn/plotting/subcluster_plot.py | 2 +- 8 files changed, 31 insertions(+), 21 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 324ff0e5..0251f8b2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -207,5 +207,5 @@ Tools: `datasets` .. autosummary:: :toctree: . - datasets.example_bcba() + datasets.visium_sge() datasets.xenium_sge() diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index b7ae9d14..c1fdec45 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -2,24 +2,34 @@ import scanpy as sc from anndata import AnnData +from scanpy.datasets._datasets import VisiumSampleID from .._settings import settings +def visium_sge( + sample_id: VisiumSampleID = "V1_Breast_Cancer_Block_A_Section_1", + *, + include_hires_tiff: bool = False, +) -> AnnData: + """Processed Visium Spatial Gene Expression data from 10x Genomics’ database. -def example_bcba() -> AnnData: - """\ - Download processed BCBA data (10X genomics published data). - Reference: - https://support.10xgenomics.com/spatial-gene-expression/datasets/1.1.0/V1_Breast_Cancer_Block_A_Section_1 - """ - settings.datasetdir.mkdir(parents=True, exist_ok=True) - filename = settings.datasetdir / "example_bcba.h5" - url = "https://www.dropbox.com/s/u3m2f16mvdom1am/example_bcba.h5ad?dl=1" - if not filename.is_file(): - sc.readwrite._download(url=url, path=filename) - adata = sc.read_h5ad(filename) - return adata + The database_ can be browsed online to find the ``sample_id`` you want. + + .. _database: https://support.10xgenomics.com/spatial-gene-expression/datasets + Parameters + ---------- + sample_id + The ID of the data sample in 10x’s spatial database. + include_hires_tiff + Download and include the high-resolution tissue image (tiff) in + `adata.uns["spatial"][sample_id]["metadata"]["source_image_path"]`. + + Returns + ------- + Annotated data matrix. + """ + return sc.datasets.visium_sge(sample_id, include_hires_tiff=include_hires_tiff) def xenium_sge( base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", diff --git a/stlearn/datasets.py b/stlearn/datasets.py index f5f99e4a..34a6ffd7 100644 --- a/stlearn/datasets.py +++ b/stlearn/datasets.py @@ -1,3 +1,3 @@ -from ._datasets._datasets import example_bcba, xenium_sge +from ._datasets._datasets import visium_sge, xenium_sge -__all__ = ["example_bcba", "xenium_sge"] +__all__ = ["visium_sge", "xenium_sge"] diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 33fa4d84..06e6271e 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -933,7 +933,7 @@ def het_plot( Examples ------------------------------------- >>> import stlearn as st - >>> adata = st.datasets.example_bcba() + >>> adata = st.datasets.visium_sge(sample_id="V1_Breast_Cancer_Block_A_Section_1") >>> pvalues = "lr_pvalues" >>> st.pl.gene_plot(adata, use_het = pvalues) diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index 2eb6a66c..b4db26c3 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -67,7 +67,7 @@ def cluster_plot( Examples ------------------------------------- >>> import stlearn as st - >>> adata = st.datasets.example_bcba() + >>> adata = st.datasets.visium_sge(sample_id="V1_Breast_Cancer_Block_A_Section_1") >>> label = "louvain" >>> st.pl.cluster_plot(adata, use_label = label, show_trajectories = True) diff --git a/stlearn/plotting/feat_plot.py b/stlearn/plotting/feat_plot.py index e47450e9..7a3d4bf7 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/plotting/feat_plot.py @@ -56,7 +56,7 @@ def feat_plot( Examples ------------------------------------- >>> import stlearn as st - >>> adata = st.datasets.example_bcba() + >>> adata = st.datasets.visium_sge(sample_id="V1_Breast_Cancer_Block_A_Section_1") >>> st.pl.gene_plot(adata, 'dpt_pseudotime') """ diff --git a/stlearn/plotting/gene_plot.py b/stlearn/plotting/gene_plot.py index cca713d0..8f4244c8 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/plotting/gene_plot.py @@ -54,7 +54,7 @@ def gene_plot( Examples ------------------------------------- >>> import stlearn as st - >>> adata = st.datasets.example_bcba() + >>> adata = st.datasets.visium_sge(sample_id="V1_Breast_Cancer_Block_A_Section_1") >>> genes = ["BRCA1","BRCA2"] >>> st.pl.gene_plot(adata, gene_symbols = genes) diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/plotting/subcluster_plot.py index 0e8c66e5..3f5eff3d 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/plotting/subcluster_plot.py @@ -50,7 +50,7 @@ def subcluster_plot( Examples ------------------------------------- >>> import stlearn as st - >>> adata = st.datasets.example_bcba() + >>> adata = st.datasets.visium_sge(sample_id="V1_Breast_Cancer_Block_A_Section_1") >>> label = "louvain" >>> cluster = 6 >>> st.pl.cluster_plot(adata, use_label = label, cluster = cluster) From 73eb7ba47f4a2c2c6856f3576b0c21a08796e55a Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 10:27:28 +1000 Subject: [PATCH 113/241] Use scanpy to get data instead of one bc sample in dropbox. --- stlearn/_datasets/_datasets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index c1fdec45..b5e5ef60 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -6,6 +6,7 @@ from .._settings import settings +# TODO - Add scanpy and covert this over. def visium_sge( sample_id: VisiumSampleID = "V1_Breast_Cancer_Block_A_Section_1", *, From c815d24fdfe31fc93cc454b4c47600e4123f05d1 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 10:33:27 +1000 Subject: [PATCH 114/241] Use scanpy to get data instead of one bc sample in dropbox. --- HISTORY.rst | 3 ++- docs/release_notes/1.1.0.rst | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index d9fe79b3..a6c64446 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,13 +8,14 @@ History * Added quality checks black, ruff and mypy and fixed appropriate source code. * Copy parameters now work with the same semantics as scanpy. * Library upgrades for leidenalg, louvain, numba, numpy, scanpy, and tensorflow. -* .datasets.xenium_sge - loads Xenium data (and caches it) similar to scanpy's visium_sge call. +* datasets.xenium_sge - loads Xenium data (and caches it) similar to scanpy.visium_sge. API and Bug Fixes: * Xenium TIFF and cell positions are now aligned. * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. +* Removed datasets.example_bcba - Replaces with wrapper for scanpy.visium_sge. 0.4.11 (2022-11-25) ------------------ diff --git a/docs/release_notes/1.1.0.rst b/docs/release_notes/1.1.0.rst index a08714b6..fc7d97dc 100644 --- a/docs/release_notes/1.1.0.rst +++ b/docs/release_notes/1.1.0.rst @@ -7,11 +7,12 @@ * Added quality checks black, ruff and mypy and fixed appropriate source code. * Copy parameters now work with the same semantics as scanpy. * Library upgrades for leidenalg, louvain, numba, numpy, scanpy, and tensorflow. -* .datasets.xenium_sge - loads Xenium data (and caches it) similar to scanpy's visium_sge call. +* datasets.xenium_sge - loads Xenium data (and caches it) similar to scanpy.visium_sge. .. rubric:: Bug fixes * Xenium TIFF and cell positions are now aligned. * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. -* pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. \ No newline at end of file +* pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. +* Removed datasets.example_bcba - Replaces with wrapper for scanpy.visium_sge. \ No newline at end of file From ab3bd6fad24e1615231199d4fd478c39184f9409 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 10:35:26 +1000 Subject: [PATCH 115/241] Use scanpy to get data instead of one bc sample in dropbox. --- stlearn/_datasets/_datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index b5e5ef60..79cb19e7 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -8,7 +8,7 @@ # TODO - Add scanpy and covert this over. def visium_sge( - sample_id: VisiumSampleID = "V1_Breast_Cancer_Block_A_Section_1", + sample_id = "V1_Breast_Cancer_Block_A_Section_1", *, include_hires_tiff: bool = False, ) -> AnnData: From 7c572a608c5aa170e5cb9bf9b92abe33bb01fd30 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 10:37:58 +1000 Subject: [PATCH 116/241] Fix datasetdir. --- stlearn/_datasets/_datasets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index 79cb19e7..392f1630 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -2,8 +2,6 @@ import scanpy as sc from anndata import AnnData -from scanpy.datasets._datasets import VisiumSampleID - from .._settings import settings # TODO - Add scanpy and covert this over. @@ -30,6 +28,7 @@ def visium_sge( ------- Annotated data matrix. """ + sc.settings.datasetdir = settings.datasetdir return sc.datasets.visium_sge(sample_id, include_hires_tiff=include_hires_tiff) def xenium_sge( @@ -52,6 +51,7 @@ def xenium_sge( library_id: Identifier for the library include_hires_tiff: Whether to download the high-res TIFF image """ + sc.settings.datasetdir = settings.datasetdir library_dir = settings.datasetdir / library_id library_dir.mkdir(parents=True, exist_ok=True) From 5f4f3a20d3c3d2cdc9fe4d48ab0737bdc498e4b4 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 10:45:50 +1000 Subject: [PATCH 117/241] No loc in colorbar. --- stlearn/plotting/classes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 89604f93..343bdc93 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -190,7 +190,6 @@ def _add_color_bar(self, plot, color_bar_label: str = ""): shrink=0.5, cmap=self.cmap, label=color_bar_label, - loc="center left" ) cb.outline.set_visible(False) From 1f9424e9624023723a131d40e4a31d91dbfb0162 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 11:15:41 +1000 Subject: [PATCH 118/241] Fix error message. --- stlearn/plotting/classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 343bdc93..1eb81c3a 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -786,7 +786,7 @@ def _add_cluster_labels(self): def _add_sub_clusters(self): if "sub_cluster_labels" not in self.query_adata.obs.columns: - raise ValueError("Please run stlearn.spatial.cluster.localization") + raise ValueError("Please run stlearn.spatial.clustering.localization") for i, label in enumerate(self.list_clusters): label_index = list( From 3fc4f60aa46cae69a19af38827420ac0bbda5a67 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 11:26:08 +1000 Subject: [PATCH 119/241] Fix calling to centroidpython. --- stlearn/plotting/classes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 1eb81c3a..87b8d22b 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -1041,7 +1041,9 @@ def _plot_subclusters(self, threshold_spots): def _add_subclusters_label(self, subset): if len(subset["sub_cluster_labels"].unique()) < 2: print("lower than 2") - centroids = [centroidpython(subset[["imagecol", "imagerow"]].values)] + imgcol = subset["imagecol"].values + imgrow = subset["imagerow"].values + centroids = [centroidpython(imgcol, imgrow)] classes = np.array([subset["sub_cluster_labels"][0]]) else: From 24c402f03536b4f5bb16e80a2f7f0fb4698aca34 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 11:52:45 +1000 Subject: [PATCH 120/241] Fix bug. --- stlearn/plotting/classes.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index 87b8d22b..eb5528ed 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -1039,8 +1039,11 @@ def _plot_subclusters(self, threshold_spots): return subset def _add_subclusters_label(self, subset): - if len(subset["sub_cluster_labels"].unique()) < 2: - print("lower than 2") + unique_subcluster_labels = len(subset["sub_cluster_labels"].unique()) + if unique_subcluster_labels == 1: + print("No unique labels found") + return + elif unique_subcluster_labels == 1: imgcol = subset["imagecol"].values imgrow = subset["imagerow"].values centroids = [centroidpython(imgcol, imgrow)] From afb8a9f6070b4352d4eacb65ab3988d06b49ac16 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 5 Jul 2025 16:25:54 +1000 Subject: [PATCH 121/241] Fixup use of categories from cluster names in plotting and make consistent in pseudotime implementations - especially uns["split_node"]. --- stlearn/_datasets/_datasets.py | 25 +++++---- stlearn/plotting/classes.py | 44 ++++++++------- stlearn/plotting/cluster_plot.py | 2 +- .../plotting/trajectory/pseudotime_plot.py | 55 +++++++------------ stlearn/plotting/utils.py | 20 +++---- stlearn/spatials/clustering/localization.py | 2 +- stlearn/spatials/trajectory/global_level.py | 12 ++-- stlearn/spatials/trajectory/pseudotime.py | 38 ++++++------- .../trajectory/shortest_path_spatial_PAGA.py | 18 +----- stlearn/tools/microenv/cci/analysis.py | 2 +- 10 files changed, 92 insertions(+), 126 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index 392f1630..a3a4d054 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -2,13 +2,15 @@ import scanpy as sc from anndata import AnnData + from .._settings import settings + # TODO - Add scanpy and covert this over. def visium_sge( - sample_id = "V1_Breast_Cancer_Block_A_Section_1", - *, - include_hires_tiff: bool = False, + sample_id="V1_Breast_Cancer_Block_A_Section_1", + *, + include_hires_tiff: bool = False, ) -> AnnData: """Processed Visium Spatial Gene Expression data from 10x Genomics’ database. @@ -31,13 +33,14 @@ def visium_sge( sc.settings.datasetdir = settings.datasetdir return sc.datasets.visium_sge(sample_id, include_hires_tiff=include_hires_tiff) + def xenium_sge( - base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", - image_filename="he_image.ome.tif", - alignment_filename="he_imagealignment.csv", - zip_filename="outs.zip", - library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", - include_hires_tiff: bool = False, + base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", + image_filename="he_image.ome.tif", + alignment_filename="he_imagealignment.csv", + zip_filename="outs.zip", + library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", + include_hires_tiff: bool = False, ): """ Download and extract Xenium SGE data files. Unlike scanpy this current does not @@ -64,8 +67,8 @@ def xenium_sge( if not all_sge_files_exist: download_filenames.append(zip_filename) if include_hires_tiff and ( - not (library_dir / alignment_filename).exists() - or not (library_dir / image_filename).exists() + not (library_dir / alignment_filename).exists() + or not (library_dir / image_filename).exists() ): download_filenames += [alignment_filename, image_filename] diff --git a/stlearn/plotting/classes.py b/stlearn/plotting/classes.py index eb5528ed..53eff406 100644 --- a/stlearn/plotting/classes.py +++ b/stlearn/plotting/classes.py @@ -32,7 +32,7 @@ def __init__( figsize: tuple[float, float] | None = None, cmap: str = "Spectral_r", use_label: str | None = None, - list_clusters: list | None = None, + list_clusters: str | list[str] | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool = True, @@ -54,6 +54,7 @@ def __init__( super().__init__( adata, ) + self.title = title self.figsize = figsize self.image_alpha = image_alpha @@ -76,17 +77,16 @@ def __init__( ), "Please choose the right label in `adata.obs.columns`!" self.use_label = use_label + unique_categories = np.array(self.adata[0].obs[use_label].cat.categories) + if self.list_clusters is None: - self.list_clusters = np.array( - self.adata[0].obs[use_label].cat.categories - ) + self.list_clusters = unique_categories else: if not isinstance(self.list_clusters, list): self.list_clusters = [self.list_clusters] clusters_indexes = [ - np.where(adata.obs[use_label].cat.categories == i)[0][0] - for i in self.list_clusters + np.where(unique_categories == i)[0][0] for i in self.list_clusters ] self.list_clusters = np.array(self.list_clusters)[ np.argsort(clusters_indexes) @@ -229,7 +229,7 @@ def __init__( figsize: tuple[float, float] | None = None, cmap: str = "Spectral_r", use_label: str | None = None, - list_clusters: list | None = None, + list_clusters: str | list[str] | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool = True, @@ -437,7 +437,7 @@ def __init__( figsize: tuple[float, float] | None = None, cmap: str = "Spectral_r", use_label: str | None = None, - list_clusters: list | None = None, + list_clusters: str | list[str] | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool = True, @@ -598,7 +598,7 @@ def __init__( figsize: tuple[float, float] | None = None, cmap: str = "default", use_label: str | None = None, - list_clusters: list | None = None, + list_clusters: str | list[str] | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool = True, @@ -708,9 +708,7 @@ def _plot_clusters(self): ] if self.use_label + "_colors" in self.adata[0].uns: - label_set = ( - self.adata[0].obs[self.use_label].cat.categories.values.astype(str) - ) + label_set = self.adata[0].obs[self.use_label].cat.categories.values col_index = np.where(label_set == cluster[0])[0][0] color = self.adata[0].uns[self.use_label + "_colors"][col_index] else: @@ -907,19 +905,23 @@ def _add_trajectories(self): ) if self.show_node: - for x, y in centroid_dict.items(): - if x in get_node(self.list_clusters, self.adata[0].uns["split_node"]): + for node, pos in centroid_dict.items(): + if str(node) in get_node( + self.list_clusters, self.adata[0].uns["split_node"] + ): self.ax.text( - y[0], - y[1], - get_cluster(str(x), self.adata[0].uns["split_node"]), + pos[0], + pos[1], + get_cluster(str(node), self.adata[0].uns["split_node"]), color="black", fontsize=8, zorder=100, bbox=dict( facecolor=cmap( int( - get_cluster(str(x), self.adata[0].uns["split_node"]) + get_cluster( + str(node), self.adata[0].uns["split_node"] + ) ) / (len(used_colors) - 1) ), @@ -945,7 +947,7 @@ def __init__( figsize: tuple[float, float] | None = None, cmap: str = "jet", use_label: str | None = None, - list_clusters: list | None = None, + list_clusters: str | list[str] | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool = True, @@ -1104,7 +1106,7 @@ def __init__( figsize: tuple[float, float] | None = None, cmap: str = "Spectral_r", use_label: str | None = None, - list_clusters: list | None = None, + list_clusters: str | list[str] | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool = True, @@ -1174,7 +1176,7 @@ def __init__( title: Optional["str"] = None, figsize: tuple[float, float] | None = None, cmap: str = "Spectral_r", - list_clusters: list | None = None, + list_clusters: str | list[str] | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool = True, diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/plotting/cluster_plot.py index b4db26c3..1293dab6 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/plotting/cluster_plot.py @@ -21,7 +21,7 @@ def cluster_plot( figsize: tuple[float, float] | None = None, cmap: str = "default", use_label: str | None = None, - list_clusters: list | None = None, + list_clusters: str | list[str] | None = None, ax: matplotlib.axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, show_plot: bool = True, diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/plotting/trajectory/pseudotime_plot.py index 802d0463..ee72a426 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/plotting/trajectory/pseudotime_plot.py @@ -5,6 +5,7 @@ from matplotlib import pyplot as plt from numpy._typing import NDArray +from stlearn.plotting.utils import get_cluster, get_node from stlearn.utils import _read_graph @@ -12,7 +13,7 @@ def pseudotime_plot( adata: AnnData, library_id: str | None = None, use_label: str = "louvain", - pseudotime_key: str = "pseudotime_key", + pseudotime_key: str = "dpt_pseudotime", list_clusters: str | list[str] | None = None, cell_alpha: float = 1.0, image_alpha: float = 1.0, @@ -32,7 +33,6 @@ def pseudotime_plot( dpi: int = 150, output: str | None = None, name: str | None = None, - copy: bool = False, ax=None, ) -> AnnData | None: """\ @@ -78,8 +78,6 @@ def pseudotime_plot( The output folder of the plot. name The filename of the plot. - copy - Return a copy instead of writing to adata. Returns ------- Nothing @@ -87,8 +85,7 @@ def pseudotime_plot( checked_list_clusters: list[str] if list_clusters is None: - unique_labels = adata.obs[use_label].unique() - checked_list_clusters = [str(i) for i in range(len(unique_labels))] + checked_list_clusters = adata.obs[use_label].cat.categories elif isinstance(list_clusters, str): checked_list_clusters = [list_clusters] else: @@ -102,7 +99,9 @@ def pseudotime_plot( labels = nx.get_edge_attributes(G, "weight") result = [] - query_node = get_node(checked_list_clusters, adata.uns["split_node"]) + query_node = list( + map(int, get_node(checked_list_clusters, adata.uns["split_node"])) + ) for edge in G.edges(query_node): if (edge[0] in query_node) and (edge[1] in query_node): result.append(edge) @@ -158,18 +157,18 @@ def pseudotime_plot( edge_color="#333333", ) - for x, y in centroid_dict.items(): - if x in get_node(checked_list_clusters, adata.uns["split_node"]): + for node, pos in centroid_dict.items(): + if str(node) in get_node(checked_list_clusters, adata.uns["split_node"]): a.text( - y[0], - y[1], - get_cluster(x, adata.uns["split_node"]), + pos[0], + pos[1], + get_cluster(str(node), adata.uns["split_node"]), color="white", fontsize=node_size, zorder=100, bbox=dict( facecolor=cmap( - int(get_cluster(x, adata.uns["split_node"])) + int(get_cluster(str(node), adata.uns["split_node"])) / (len(used_colors) - 1) ), boxstyle="circle", @@ -221,18 +220,20 @@ def pseudotime_plot( ) if show_node: - for x, y in centroid_dict.items(): - if x in get_node(checked_list_clusters, adata.uns["split_node"]): + for node, pos in centroid_dict.items(): + if str(node) in get_node( + checked_list_clusters, adata.uns["split_node"] + ): a.text( - y[0], - y[1], - get_cluster(x, adata.uns["split_node"]), + pos[0], + pos[1], + str(get_cluster(str(node), adata.uns["split_node"])), color="black", fontsize=8, zorder=100, bbox=dict( facecolor=cmap( - int(get_cluster(x, adata.uns["split_node"])) + get_cluster(str(node), adata.uns["split_node"]) / (len(used_colors) - 1) ), boxstyle="circle", @@ -274,19 +275,3 @@ def pseudotime_plot( plt.show() return adata - - -# get name of cluster by subcluster -def get_cluster(search: int, split_node: dict[str, list[str]]): - for cl, sub in split_node.items(): - if str(search) in sub: - return cl - - -def get_node( - node_list: list[str], split_node: dict[str, list[str]] -) -> NDArray[np.int64]: - all_values = [] - for node in node_list: - all_values.extend(split_node[node]) - return np.array([int(val) for val in all_values], dtype=np.int64) diff --git a/stlearn/plotting/utils.py b/stlearn/plotting/utils.py index e819a8be..a1380899 100644 --- a/stlearn/plotting/utils.py +++ b/stlearn/plotting/utils.py @@ -31,22 +31,18 @@ def centroidpython(x, y): return sum(x) / length_of_x, sum(y) / length_of_x -def get_cluster(search, dictionary): - for ( - cl, - sub, - ) in ( - dictionary.items() - ): # for name, age in dictionary.iteritems(): (for Python 2.x) - if search in sub: +# get name of cluster by subcluster +def get_cluster(search: str, split_node: dict[str, list[str]]): + for cl, sub in split_node.items(): + if str(search) in sub: return cl -def get_node(node_list, split_node): - result = np.array([]) +def get_node(node_list: list[str], split_node: dict[str, list[str]]) -> list[str]: + all_values = [] for node in node_list: - result = np.append(result, np.array(split_node[node]).astype(int)) - return result.astype(int) + all_values.extend(split_node[node]) + return all_values def check_sublist(full, sub): diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatials/clustering/localization.py index f2594454..c45757e9 100644 --- a/stlearn/spatials/clustering/localization.py +++ b/stlearn/spatials/clustering/localization.py @@ -81,7 +81,7 @@ def localization( ), ) - labels_cat = adata.obs[use_label].cat.categories + labels_cat = list(map(int, adata.obs[use_label].cat.categories)) cat_ind = {labels_cat[i]: i for i in range(len(labels_cat))} adata.uns[use_label + "_index_dict"] = cat_ind diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatials/trajectory/global_level.py index 16308966..1a77985b 100644 --- a/stlearn/spatials/trajectory/global_level.py +++ b/stlearn/spatials/trajectory/global_level.py @@ -55,7 +55,7 @@ def global_level( query_nodes = list(cat_inds.values()) else: if isinstance(list_clusters[0], str): - list_clusters = [cat_inds[label] for label in list_clusters] + list_clusters = [cat_inds[int(label)] for label in list_clusters] query_nodes = list_clusters query_nodes = ordering_nodes(query_nodes, use_label, adata) @@ -75,19 +75,19 @@ def global_level( ].unique(): query_dict[int(j)] = int(i) order_dict[int(j)] = int(order) - order += 1 dm_list = [] sdm_list = [] order_big_dict = {} edge_list = [] + split_node = adata.uns["split_node"] for i, j in enumerate(query_nodes): order_big_dict[j] = int(i) if i == len(query_nodes) - 1: break - for j in adata.uns["split_node"][query_nodes[i]]: - for k in adata.uns["split_node"][query_nodes[i + 1]]: + for j in split_node[str(query_nodes[i])]: + for k in split_node[str(query_nodes[i + 1])]: edge_list.append((int(j), int(k))) # Calculate DPT distance matrix @@ -123,7 +123,7 @@ def global_level( ) H_sub = nx.DiGraph(H_sub) prepare_root = [] - for node in adata.uns["split_node"][query_nodes[0]]: + for node in split_node[str(query_nodes[0])]: H_sub.add_edge(9999, int(node)) prepare_root.append(centroid_dict[int(node)]) @@ -141,7 +141,7 @@ def global_level( H_sub = nx.DiGraph(H_sub) prepare_root = [] - for node in adata.uns["split_node"][query_nodes[0]]: + for node in split_node[str(query_nodes[0])]: H_sub.add_edge(9999, int(node)) prepare_root.append(centroid_dict[int(node)]) diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatials/trajectory/pseudotime.py index b2641791..f70e2a8f 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatials/trajectory/pseudotime.py @@ -1,15 +1,19 @@ import networkx as nx import numpy as np import pandas as pd -import scanpy +import scanpy as sc from anndata import AnnData +from sklearn.neighbors import NearestCentroid +from stlearn.pp import neighbors +from stlearn.spatials.clustering import localization +from stlearn.spatials.morphology import adjust from stlearn.types import _METHOD def pseudotime( adata: AnnData, - use_label: str | None = None, + use_label: str = "louvain", eps: float = 20, n_neighbors: int = 25, use_rep: str = "X_pca", @@ -68,30 +72,21 @@ def pseudotime( except: pass - assert use_label is not None, "Please choose the right `use_label`!" - - # Localize - from stlearn.spatials.clustering import localization - - if "sub_clusters_laber" not in adata.obs.columns: + if "sub_cluster_labels" not in adata.obs.columns: localization(adata, use_label=use_label, eps=eps) # Running knn if run_knn: - from stlearn.pp import neighbors - neighbors(adata, n_neighbors=n_neighbors, use_rep=use_rep, random_state=0) # Running paga - scanpy.tl.paga(adata, groups=use_label) + sc.tl.paga(adata, groups=use_label) # Denoising the graph - scanpy.tl.diffmap(adata) + sc.tl.diffmap(adata) if use_sme: - from stlearn.spatials.morphology import adjust - adjust(adata, use_data="X_diffmap", radius=radius, method=method) adata.obsm["X_diffmap"] = adata.obsm["X_diffmap_morphology"] @@ -103,9 +98,9 @@ def pseudotime( cnt_matrix = pd.DataFrame(cnt_matrix) # Mapping louvain label to subcluster - cat_ind = adata.uns[use_label + "_index_dict"] + cat_inds = adata.uns[use_label + "_index_dict"] split_node = {} - for label in adata.obs[use_label].unique(): + for label in adata.obs[use_label].cat.categories: meaningful_sub = [] for i in adata.obs[adata.obs[use_label] == label][ "sub_cluster_labels" @@ -116,10 +111,12 @@ def pseudotime( ): meaningful_sub.append(i) - split_node[cat_ind[label]] = meaningful_sub + label = cat_inds[int(label)] + split_node[label] = meaningful_sub adata.uns["threshold_spots"] = threshold_spots - adata.uns["split_node"] = split_node + # split_node has string keys for rest of code/plotting (names a strings) + adata.uns["split_node"] = {str(k): v for k, v in split_node.items()} # Replicate louvain label row to prepare for subcluster connection # matrix construction @@ -155,8 +152,6 @@ def pseudotime( adata.uns["global_graph"]["node_dict"] = node_convert # Create centroid dict for subclusters - from sklearn.neighbors import NearestCentroid - clf = NearestCentroid() clf.fit(adata.obs[["imagecol", "imagerow"]].values, adata.obs["sub_cluster_labels"]) centroid_dict = dict(zip(clf.classes_.astype(int), clf.centroids_)) @@ -174,10 +169,9 @@ def closest_node(node, nodes): centroid_dict[int(cl)] = new_centroid adata.uns["centroid_dict"] = centroid_dict - centroid_dict = {int(key): centroid_dict[key] for key in centroid_dict} # Running diffusion pseudo-time - scanpy.tl.dpt(adata) + sc.tl.dpt(adata) if reverse: adata.obs[pseudotime_key] = 1 - adata.obs[pseudotime_key] diff --git a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py b/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py index 7d7ea2ae..8daef15b 100644 --- a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py +++ b/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py @@ -1,6 +1,6 @@ import networkx as nx -import numpy as np +from stlearn.plotting.utils import get_node from stlearn.utils import _read_graph @@ -26,7 +26,7 @@ def shortest_path_spatial_PAGA( key ].max() - # Force original PAGA to directed PAGA based on pseudotime + # Force original PAGA to a directed PAGA based on pseudotime edge_to_remove = [] for edge in H.edges: if node_pseudotime[edge[0]] - node_pseudotime[edge[1]] > 0: @@ -72,20 +72,6 @@ def shortest_path_spatial_PAGA( return shortest_path.split(",") -# get name of cluster by subcluster -def get_cluster(search, dictionary): - for cl, sub in dictionary.items(): - if search in sub: - return cl - - -def get_node(node_list, split_node): - result = np.array([]) - for node in node_list: - result = np.append(result, np.array(split_node[int(node)]).astype(int)) - return result.astype(int) - - def find_min_max_node(adata, key="dpt_pseudotime", use_label="leiden"): min_cluster = int(adata.obs[adata.obs[key] == 0][use_label].values[0]) max_cluster = int(adata.obs[adata.obs[key] == 1][use_label].values[0]) diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tools/microenv/cci/analysis.py index 3315d253..0905202c 100644 --- a/stlearn/tools/microenv/cci/analysis.py +++ b/stlearn/tools/microenv/cci/analysis.py @@ -174,7 +174,7 @@ def grid( grid_data.obs[use_label] = [cell_set[index] for index in max_indices] grid_data.obs[use_label] = grid_data.obs[use_label].astype("category") grid_data.obs[use_label] = grid_data.obs[use_label].cat.set_categories( - list(adata.obs[use_label].cat.categories) + adata.obs[use_label].cat.categories ) if f"{use_label}_colors" in adata.uns: grid_data.uns[f"{use_label}_colors"] = adata.uns[f"{use_label}_colors"] From e6f4109cfc54bebbfb08db13f33cd73baeecd202 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sun, 6 Jul 2025 13:27:00 +1000 Subject: [PATCH 122/241] Change format of lrfeatures (so that it persists) and cleanup. --- stlearn/__init__.py | 2 +- stlearn/plotting/cci_plot.py | 85 ++---------------------- stlearn/plotting/cci_plot_helpers.py | 53 ++++----------- stlearn/tl.py | 2 + stlearn/tools/cache/__init__.py | 6 ++ stlearn/tools/cache/anndata.py | 51 ++++++++++++++ stlearn/tools/microenv/cci/perm_utils.py | 16 +++-- 7 files changed, 87 insertions(+), 128 deletions(-) create mode 100644 stlearn/tools/cache/__init__.py create mode 100644 stlearn/tools/cache/anndata.py diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 51f1e6f3..213fe82f 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -1,6 +1,6 @@ """Top-level package for stLearn.""" -__author__ = """Genomics and Machine Learning lab""" +__author__ = """Genomics and Machine Learning Lab""" __email__ = "andrew.newman@uq.edu.au" __version__ = "1.1.0" diff --git a/stlearn/plotting/cci_plot.py b/stlearn/plotting/cci_plot.py index 06e6271e..95fde072 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/plotting/cci_plot.py @@ -132,7 +132,7 @@ def lr_summary( A list of LRs to highlight on the plot, will added text and change color of points for these LRs. Useful for highlighting LRs of interest. y: str - The way to rank the LRs, default is by the no. of signifcant spots, + The way to rank the LRs, default is by the no. of significant spots, but can be any column in adata.uns['lr_summary']. color: str The color of the points. @@ -1379,13 +1379,13 @@ def lr_chord_plot( Each cell type has a labelled edge taking up a proportion of the outter circle. Chords connecting cell type edges are coloured by the dominant sending cell. - Each chord linking cell types has an assymetric shape. + Each chord linking cell types has an asymmetric shape. For two cell types, A and B, the side of the chord attached to edge A is sized by the total interactions from B->A, where B is expressing the ligand & A is expressing the receptor. - Hence, the proportion of a cell type's edge in the chordplot circle + Hence, the proportion of a cell type's edge in the chord plot circle represents the total input signals to that cell type; while the - area of the chordplot circle taken up by the outputted chords from a given + area of the chord plot circle taken up by the outputted chords from a given cell type represents the total output signals from that cell type. Parameters @@ -1550,7 +1550,7 @@ def grid_plot( return fig, ax -####################### Bokeh Interactive Plots ################################ +# Bokeh Interactive Plots def lr_plot_interactive(adata: AnnData): """Plots the LR scores for significant spots interatively using Bokeh. @@ -1575,78 +1575,3 @@ def spatialcci_plot_interactive(adata: AnnData): bokeh_object = BokehSpatialCciPlot(adata) output_notebook() show(bokeh_object.app, notebook_handle=True) - - -# def het_plot_interactive(adata: AnnData): -# bokeh_object = BokehCciPlot(adata) -# output_notebook() -# show(bokeh_object.app, notebook_handle=True) - - -# Bokeh & old grid plots; -# has not been tested since multi-LR testing implimentation. - -# def het_plot_interactive(adata: AnnData): -# bokeh_object = BokehCciPlot(adata) -# output_notebook() -# show(bokeh_object.app, notebook_handle=True) - - -# def grid_plot( -# adata: AnnData, -# use_het: str = None, -# num_row: int = 10, -# num_col: int = 10, -# vmin: float = None, -# vmax: float = None, -# cropped: bool = True, -# margin: int = 100, -# dpi: int = 100, -# name: str = None, -# output: str = None, -# copy: bool = False, -# ) -> Optional[AnnData]: -# -# """ -# Cell diversity plot for sptial transcriptomics data. -# -# Parameters -# ---------- -# adata: Annotated data matrix. -# use_het: Cluster heterogeneity count results from tl.cci_rank.het -# num_row: int Number of grids on height -# num_col: int Number of grids on width -# cropped crop image or not. -# margin margin used in cropping. -# dpi: Set dpi as the resolution for the plot. -# name: Name of the output figure file. -# output: Save the figure as file or not. -# copy: Return a copy instead of writing to adata. -# -# Returns -# ------- -# Nothing -# """ -# -# try: -# import seaborn as sns -# except: -# raise ImportError("Please run `pip install seaborn`") -# plt.subplots() -# -# sns.heatmap( -# pd.DataFrame(np.array(adata.obsm[use_het]).reshape(num_col, num_row)).T, -# vmin=vmin, -# vmax=vmax, -# ) -# plt.axis("equal") -# -# if output is not None: -# plt.savefig( -# output + "/" + name + "_heatmap.pdf", -# dpi=dpi, -# bbox_inches="tight", -# pad_inches=0, -# ) -# -# plt.show() diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index 85efe6ae..ead0b2bc 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -35,19 +35,25 @@ def lr_scatter( figsize: tuple | None = None, show_all: bool = False, ): - """General plotting of the LR features.""" - highlight = highlight_lrs is not None - if not highlight: + lr_df = data.uns["lr_summary"] + + if max_text > len(lr_df): + print(f"Note: max_text ({max_text}) exceeds available LRs ({len(lr_df)})") + + if highlight_lrs is None: show_text = show_text if n_top <= max_text else False else: + missing_lrs = [lr for lr in highlight_lrs if lr not in lr_df.index] + if missing_lrs: + raise ValueError( + f"The following highlight_lrs are not found in lr_summary index: {missing_lrs}") highlight_lrs = highlight_lrs[0:max_text] - lr_df = data.uns["lr_summary"] lrs = lr_df.index.values.astype(str)[0:n_top] lr_features = data.uns["lrfeatures"] lr_df = pd.concat([lr_df, lr_features], axis=1).loc[lrs, :] if feature not in lr_df.columns: - raise Exception(f"Inputted {feature}; must be one of {list(lr_df.columns)}") + raise ValueError(f"Inputted {feature}; must be one of {list(lr_df.columns)}") rot = 90 if feature != "n_spots_sig" else 70 @@ -72,39 +78,6 @@ def lr_scatter( pad=0, show_all=show_all, ) - # ranks = np.array(list(range(len(n_spots)))) - # - # if type(lr_text_fp)==type(None): - # lr_text_fp = {'weight': 'bold', 'size': 8} - # if type(axis_text_fp)==type(None): - # axis_text_fp = {'weight': 'bold', 'size': 12} - # - # if type(ax)==type(None): - # width = (7.5 / 50) * n_top if show_text and not highlight else 7.5 - # if width > 20: - # width = 20 - # fig, ax = plt.subplots(figsize=(width, 4)) - # - # # Plotting the points # - # ax.scatter(ranks, n_spots, alpha=alpha, c=color) - # - # if show_text: - # if highlight: - # ranks = ranks[[np.where(lrs==lr)[0][0] for lr in highlight_lrs]] - # ax.scatter(ranks, n_spots[ranks], alpha=alpha, c=highlight_color) - # - # for i in ranks: - # ax.text(i-.2, n_spots[i], lrs[i], rotation=rot, fontdict=lr_text_fp) - # - # ax.spines['top'].set_visible(False) - # ax.spines['right'].set_visible(False) - # ax.set_xlabel('LR Rank', axis_text_fp) - # ax.set_ylabel(feature, axis_text_fp) - # - # if show: - # plt.show() - # else: - # return ax def rank_scatter( @@ -161,7 +134,7 @@ def rank_scatter( y_max = y_max + y_max * pad ax.set_ylim(y_min, y_max) if point_sizes is not None: - # produce a legend with a cross section of sizes from the scatter + # produce a legend with a cross-section of sizes from the scatter handles, labels = scatter.legend_elements(prop="sizes", alpha=0.6, num=4) [handle.set_markeredgecolor("none") for handle in handles] starts = [label.find("{") for label in labels] @@ -257,7 +230,7 @@ def add_arrows( # in the base plotting function class. # Reason why is because scale_factor refers to scaling the # image to match the spot spatial coordinates, not the - # the spots to match the image coordinates!!! + # spots to match the image coordinates!!! L_bool = l_expr > min_expr R_bool = r_expr > min_expr diff --git a/stlearn/tl.py b/stlearn/tl.py index 3a3c0d9e..5aa3002f 100644 --- a/stlearn/tl.py +++ b/stlearn/tl.py @@ -1,8 +1,10 @@ +from .tools import cache from .tools import clustering from .tools.label import label from .tools.microenv import cci __all__ = [ + "cache", "clustering", "cci", "label", diff --git a/stlearn/tools/cache/__init__.py b/stlearn/tools/cache/__init__.py new file mode 100644 index 00000000..42d33af2 --- /dev/null +++ b/stlearn/tools/cache/__init__.py @@ -0,0 +1,6 @@ +from .anndata import write_subset_h5ad, merge_h5ad_into_adata + +__all__ = [ + "write_subset_h5ad", + "merge_h5ad_into_adata", +] diff --git a/stlearn/tools/cache/anndata.py b/stlearn/tools/cache/anndata.py new file mode 100644 index 00000000..8916aa74 --- /dev/null +++ b/stlearn/tools/cache/anndata.py @@ -0,0 +1,51 @@ +import anndata as ad +import numpy as np +import pandas as pd + + +def write_subset_h5ad(adata, filename, obsm_keys=None, uns_keys=None): + """Write only specific obsm and uns components to H5AD""" + + # Create a minimal AnnData object with the same structure + minimal_adata = ad.AnnData( + X=np.zeros((adata.n_obs, 1)), + obs=adata.obs.index.to_frame(name='cell_id'), + var=pd.DataFrame(index=['placeholder']) + ) + + if obsm_keys: + for key in obsm_keys: + if key in adata.obsm: + value = adata.obsm[key] + if isinstance(value, list): + value = np.array(value) + minimal_adata.obsm[key] = value + print(f"Added obsm['{key}'] with shape {value.shape}") + else: + print(f"Warning: obsm['{key}'] not found") + + if uns_keys: + for key in uns_keys: + if key in adata.uns: + minimal_adata.uns[key] = adata.uns[key] + print(f"Added uns['{key}']") + else: + print(f"Warning: uns['{key}'] not found") + + minimal_adata.write_h5ad(filename, compression='gzip', compression_opts=9) + print(f"Wrote subset to {filename}") + + +def merge_h5ad_into_adata(adata_main, h5ad_file): + adata_subset = ad.read_h5ad(h5ad_file) + print(f"Reading {h5ad_file}") + + for key, value in adata_subset.obsm.items(): + adata_main.obsm[key] = value + print(f"Added obsm['{key}'] with shape {value.shape}") + + for key, value in adata_subset.uns.items(): + adata_main.uns[key] = value + print(f"Added uns['{key}']") + + return adata_main diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index 426512a3..53a473a0 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -319,17 +319,19 @@ def get_lr_features(adata, lr_expr, lrs, quantiles): # Saving the lrfeatures... cols = ["nonzero-median", "zero-prop", "median_rank", "prop_rank", "mean_rank"] - lr_features = pd.DataFrame(index=lrs, columns=cols) - lr_features.iloc[:, 0] = lr_median_means - lr_features.iloc[:, 1] = lr_prop_means - lr_features.iloc[:, 2] = np.array(median_ranks) - lr_features.iloc[:, 3] = np.array(prop_ranks) - lr_features.iloc[:, 4] = np.array(mean_ranks) + lr_features_data = { + cols[0]: np.array(lr_median_means, dtype=np.float64), + cols[1]: np.array(lr_prop_means, dtype=np.float64), + cols[2]: np.array(median_ranks, dtype=np.float64), + cols[3]: np.array(prop_ranks, dtype=np.float64), + cols[4]: np.array(mean_ranks, dtype=np.float64) + } + lr_features = pd.DataFrame(lr_features_data, index=lrs) lr_features = lr_features.iloc[np.argsort(mean_ranks), :] lr_cols = [f"L_{quant}" for quant in quantiles] + [ f"R_{quant}" for quant in quantiles ] - quant_df = pd.DataFrame(lr_quants, columns=lr_cols, index=lrs) + quant_df = pd.DataFrame(lr_quants, columns=lr_cols, index=lrs, dtype=np.float64) lr_features = pd.concat((lr_features, quant_df), axis=1) adata.uns["lrfeatures"] = lr_features From cae052e2ffd3836633474539e36df81ebb880a95 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sun, 6 Jul 2025 17:00:54 +1000 Subject: [PATCH 123/241] Fix formatting. --- stlearn/_datasets/_datasets.py | 22 +++++++++++----------- stlearn/plotting/cci_plot_helpers.py | 4 +++- stlearn/tl.py | 3 +-- stlearn/tools/cache/__init__.py | 2 +- stlearn/tools/cache/anndata.py | 6 +++--- stlearn/tools/microenv/cci/perm_utils.py | 2 +- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/stlearn/_datasets/_datasets.py b/stlearn/_datasets/_datasets.py index a3a4d054..56f17fd5 100644 --- a/stlearn/_datasets/_datasets.py +++ b/stlearn/_datasets/_datasets.py @@ -8,9 +8,9 @@ # TODO - Add scanpy and covert this over. def visium_sge( - sample_id="V1_Breast_Cancer_Block_A_Section_1", - *, - include_hires_tiff: bool = False, + sample_id="V1_Breast_Cancer_Block_A_Section_1", + *, + include_hires_tiff: bool = False, ) -> AnnData: """Processed Visium Spatial Gene Expression data from 10x Genomics’ database. @@ -35,12 +35,12 @@ def visium_sge( def xenium_sge( - base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", - image_filename="he_image.ome.tif", - alignment_filename="he_imagealignment.csv", - zip_filename="outs.zip", - library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", - include_hires_tiff: bool = False, + base_url="https://cf.10xgenomics.com/samples/xenium/1.0.1", + image_filename="he_image.ome.tif", + alignment_filename="he_imagealignment.csv", + zip_filename="outs.zip", + library_id="Xenium_FFPE_Human_Breast_Cancer_Rep1", + include_hires_tiff: bool = False, ): """ Download and extract Xenium SGE data files. Unlike scanpy this current does not @@ -67,8 +67,8 @@ def xenium_sge( if not all_sge_files_exist: download_filenames.append(zip_filename) if include_hires_tiff and ( - not (library_dir / alignment_filename).exists() - or not (library_dir / image_filename).exists() + not (library_dir / alignment_filename).exists() + or not (library_dir / image_filename).exists() ): download_filenames += [alignment_filename, image_filename] diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/plotting/cci_plot_helpers.py index ead0b2bc..25b2a25e 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/plotting/cci_plot_helpers.py @@ -46,7 +46,9 @@ def lr_scatter( missing_lrs = [lr for lr in highlight_lrs if lr not in lr_df.index] if missing_lrs: raise ValueError( - f"The following highlight_lrs are not found in lr_summary index: {missing_lrs}") + "The following highlight_lrs are not found in lr_summary index: " + + ",".join(missing_lrs) + ) highlight_lrs = highlight_lrs[0:max_text] lrs = lr_df.index.values.astype(str)[0:n_top] diff --git a/stlearn/tl.py b/stlearn/tl.py index 5aa3002f..b7bb3cb7 100644 --- a/stlearn/tl.py +++ b/stlearn/tl.py @@ -1,5 +1,4 @@ -from .tools import cache -from .tools import clustering +from .tools import cache, clustering from .tools.label import label from .tools.microenv import cci diff --git a/stlearn/tools/cache/__init__.py b/stlearn/tools/cache/__init__.py index 42d33af2..9fe80973 100644 --- a/stlearn/tools/cache/__init__.py +++ b/stlearn/tools/cache/__init__.py @@ -1,4 +1,4 @@ -from .anndata import write_subset_h5ad, merge_h5ad_into_adata +from .anndata import merge_h5ad_into_adata, write_subset_h5ad __all__ = [ "write_subset_h5ad", diff --git a/stlearn/tools/cache/anndata.py b/stlearn/tools/cache/anndata.py index 8916aa74..a208feb8 100644 --- a/stlearn/tools/cache/anndata.py +++ b/stlearn/tools/cache/anndata.py @@ -9,8 +9,8 @@ def write_subset_h5ad(adata, filename, obsm_keys=None, uns_keys=None): # Create a minimal AnnData object with the same structure minimal_adata = ad.AnnData( X=np.zeros((adata.n_obs, 1)), - obs=adata.obs.index.to_frame(name='cell_id'), - var=pd.DataFrame(index=['placeholder']) + obs=adata.obs.index.to_frame(name="cell_id"), + var=pd.DataFrame(index=["placeholder"]), ) if obsm_keys: @@ -32,7 +32,7 @@ def write_subset_h5ad(adata, filename, obsm_keys=None, uns_keys=None): else: print(f"Warning: uns['{key}'] not found") - minimal_adata.write_h5ad(filename, compression='gzip', compression_opts=9) + minimal_adata.write_h5ad(filename, compression="gzip", compression_opts=9) print(f"Wrote subset to {filename}") diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tools/microenv/cci/perm_utils.py index 53a473a0..6bc84d4d 100644 --- a/stlearn/tools/microenv/cci/perm_utils.py +++ b/stlearn/tools/microenv/cci/perm_utils.py @@ -324,7 +324,7 @@ def get_lr_features(adata, lr_expr, lrs, quantiles): cols[1]: np.array(lr_prop_means, dtype=np.float64), cols[2]: np.array(median_ranks, dtype=np.float64), cols[3]: np.array(prop_ranks, dtype=np.float64), - cols[4]: np.array(mean_ranks, dtype=np.float64) + cols[4]: np.array(mean_ranks, dtype=np.float64), } lr_features = pd.DataFrame(lr_features_data, index=lrs) lr_features = lr_features.iloc[np.argsort(mean_ranks), :] From e27cf0a84772e66c765d2398e75d0223c632595d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sun, 6 Jul 2025 18:45:11 +1000 Subject: [PATCH 124/241] Fixup documentation. --- .gitignore | 3 +- AUTHORS.rst | 7 +- docs/Makefile | 8 +- docs/_temp/example_cci.py | 180 --------------------- docs/_templates/autosummary/base.rst | 4 - docs/_templates/autosummary/class.rst | 5 - docs/api.rst | 211 ------------------------ docs/conf.py | 221 +++----------------------- docs/images/logo.png | Bin 0 -> 484189 bytes docs/index.rst | 51 +++--- docs/installation.rst | 2 +- docs/interactive.rst | 10 +- docs/list_tutorial.txt | 11 -- docs/make.bat | 21 ++- docs/release_notes/0.3.2.rst | 2 +- docs/release_notes/0.4.6.rst | 2 +- docs/release_notes/index.rst | 7 +- docs/requirements.txt | 15 -- docs/tutorials.rst | 22 +-- pyproject.toml | 4 + stlearn/wrapper/read.py | 4 +- 21 files changed, 84 insertions(+), 706 deletions(-) delete mode 100644 docs/_temp/example_cci.py delete mode 100644 docs/_templates/autosummary/base.rst delete mode 100644 docs/_templates/autosummary/class.rst delete mode 100644 docs/api.rst create mode 100644 docs/images/logo.png delete mode 100644 docs/list_tutorial.txt delete mode 100644 docs/requirements.txt diff --git a/.gitignore b/.gitignore index b5495d15..6fd78a11 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__/ # Distribution / packaging .Python build/ +_build/ data/samples develop-eggs/ dist/ @@ -59,7 +60,7 @@ cover/ .idea/ # Sphinx documentation -docs/_build +docs.bk/_build # Distribution/package/temporary files data/ diff --git a/AUTHORS.rst b/AUTHORS.rst index d30eaa6e..a024f3f5 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -5,9 +5,12 @@ Credits Development Lead ---------------- -* Genomics and Machine Learning lab +* Genomics and Machine Learning Lab Contributors ------------ -None yet. Why not be the first? +* Brad Balderson +* Andrew Newman +* Duy Pham +* Xiao Tan diff --git a/docs/Makefile b/docs/Makefile index 96688bf3..d4bb2cbb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,10 +1,10 @@ # Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = python -msphinx -SPHINXPROJ = stlearn +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build diff --git a/docs/_temp/example_cci.py b/docs/_temp/example_cci.py deleted file mode 100644 index 15fe9a84..00000000 --- a/docs/_temp/example_cci.py +++ /dev/null @@ -1,180 +0,0 @@ -# """ Example code for running CCI analysis using new interface/approach. - -# Tested: * Within-spot mode -# * Between-spot mode - -# TODO tests: * Above with cell heterogeneity information -# """ - -################################################################################ -# Environment setup # -################################################################################ -import stlearn as st -import matplotlib.pyplot as plt - -################################################################################ -# Load your data # -################################################################################ -# TODO - load as an AnnData & perform usual pre-processing. -data = None # replace with your code - -# """ # Adding cell heterogeneity information if you have it. -# st.add.labels(data, 'tutorials/label_transfer_bc.csv', sep='\t') -# st.pl.cluster_plot(data, use_label="predictions") -# """ - -################################################################################ -# Performing cci_rank analysis # -################################################################################ -# Load the NATMI literature-curated database of LR pairs, data formatted # -lrs = st.tl.cci.load_lrs(["connectomeDB2020_lit"]) - -st.tl.cci.run( - data, - lrs, - use_label=None, # Need to add the label transfer results to object first, above code puts into 'label_transfer' - use_het="cell_het", # Slot for cell het. results in adata.obsm, only if use_label specified - min_spots=6, # Filter out any LR pairs with no scores for less than 6 spots - distance=None, # distance=0 for within-spot mode, None to auto-select distance to nearest neighbourhood. - n_pairs=1000, # Number of random pairs to generate - adj_method="fdr_bh", # MHT correction method - min_expr=0, # min expression for gene to be considered expressed. - pval_adj_cutoff=0.05, -) -# """ -# Example output: - -# Calculating neighbours... -# 0 spots with no neighbours, 6 median spot neighbours. -# Spot neighbour indices stored in adata.uns['spot_neighbours'] -# Altogether 1393 valid L-R pairs -# Generating random gene pairs... -# Generating the background... -# Calculating p-values for each LR pair in each spot...: 100%|██████████ [ time left: 00:00 ] - -# Storing results: - -# lr_scores stored in adata.obsm['lr_scores']. -# p_vals stored in adata.obsm['p_vals']. -# p_adjs stored in adata.obsm['p_adjs']. -# -log10(p_adjs) stored in adata.obsm['-log10(p_adjs)']. -# lr_sig_scores stored in adata.obsm['lr_sig_scores']. - -# Per-spot results in adata.obsm have columns in same order as rows in adata.uns['lr_summary']. -# Summary of LR results in adata.uns['lr_summary']. -# """ - -################################################################################ -# Visualising results # -################################################################################ -# Plotting the -log10(p_adjs) for the lr with the highest number of spots. -# Set use_lr to any listed in data.uns['lr_summary'] to visualise alternate lrs. -st.pl.lr_result_plot( - data, - use_lr=None, # Which LR to use, if None then uses top resuls from data.uns['lr_results'] - use_result="-log10(p_adjs)", # Which result to visualise, must be one of - # p_vals, p_adjs, -log10(p_adjs), lr_sig_scores -) -plt.show() - -################################################################################ -# Extra diagnostic plots for results # -################################################################################ -# TODO: -# Below needs to be updated with new way of storing results. - -# Looking at which LR pairs were significant across the most spots # -print(data.uns["lr_summary"]) # Rank-ordered by pairs with most significant spots - -# Now looking at the LR pair with the highest number of sig. spots # -best_lr = data.uns["lr_summary"].index.values[0] - -# Binary LR coexpression plot for all spots # -st.pl.lr_plot( - data, - best_lr, - inner_size_prop=0.1, - outer_mode="binary", - pt_scale=10, - use_label=None, - show_image=True, - sig_spots=False, -) -plt.show() - -# Significance scores for all spots # -st.pl.lr_plot( - data, - best_lr, - inner_size_prop=1, - outer_mode=None, - pt_scale=20, - use_label="lr_scores", - show_image=True, - sig_spots=False, -) -plt.show() - -# Binary LR coexpression plot for significant spots # -st.pl.lr_plot( - data, - best_lr, - outter_size_prop=1, - outer_mode="binary", - pt_scale=20, - use_label=None, - show_image=True, - sig_spots=True, -) -plt.show() - -# Continuous LR coexpression for signficant spots # -st.pl.lr_plot( - data, - best_lr, - inner_size_prop=0.1, - middle_size_prop=0.2, - outter_size_prop=0.4, - outer_mode="continuous", - pt_scale=150, - use_label=None, - show_image=True, - sig_spots=True, -) -plt.show() - -# Continous LR coexpression for significant spots with tissue_type information # -st.pl.lr_plot( - data, - best_lr, - inner_size_prop=0.08, - middle_size_prop=0.3, - outter_size_prop=0.5, - outer_mode="continuous", - pt_scale=150, - use_label="tissue_type", - show_image=True, - sig_spots=True, -) -plt.show() - - -# # Old version of visualisation # -# """ -# # LR enrichment scores -# data.obsm[f'{best_lr}_scores'] = data.uns['per_lr_results'][best_lr].loc[:, -# 'lr_scores'].values -# # -log10(p_adj) of LR enrichment scores -# data.obsm[f'{best_lr}_log-p_adj'] = data.uns['per_lr_results'][best_lr].loc[:, -# '-log10(p_adj)'].values -# # Significant LR enrichment scores -# data.obsm[f'{best_lr}_sig-scores'] = data.uns['per_lr_results'][best_lr].loc[:, -# 'lr_sig_scores'].values - -# # Visualising these results # -# st.pl.het_plot(data, use_het=f'{best_lr}_scores', cell_alpha=0.7) -# plt.show() - -# st.pl.het_plot(data, use_het=f'{best_lr}_sig-scores', cell_alpha=0.7) -# plt.show() -# """ diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst deleted file mode 100644 index 7a780868..00000000 --- a/docs/_templates/autosummary/base.rst +++ /dev/null @@ -1,4 +0,0 @@ - -{% extends "!autosummary/base.rst" %} - -.. http://www.sphinx-doc.org/en/stable/ext/autosummary.html#customizing-templates diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst deleted file mode 100644 index 42c37f16..00000000 --- a/docs/_templates/autosummary/class.rst +++ /dev/null @@ -1,5 +0,0 @@ -{{ fullname | escape | underline}} - -.. currentmodule:: {{ module }} - -.. add toctree option to make autodoc generate the pages diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index 0251f8b2..00000000 --- a/docs/api.rst +++ /dev/null @@ -1,211 +0,0 @@ -.. module:: stlearn -.. automodule:: stlearn - :noindex: - -API -====================================== - -Import stLearn as:: - - import stlearn as st - - -Wrapper functions: `wrapper` ------------------------------- - -.. module:: stlearn.wrapper -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - Read10X - ReadOldST - ReadSlideSeq - ReadMERFISH - ReadSeqFish - convert_scanpy - create_stlearn - - -Add: `add` -------------------- - -.. module:: stlearn.add -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - add.image - add.positions - add.parsing - add.lr - add.labels - add.annotation - add.add_loupe_clusters - add.add_mask - add.apply_mask - add.add_deconvolution - - -Preprocessing: `pp` -------------------- - -.. module:: stlearn.pp -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - pp.filter_genes - pp.log1p - pp.normalize_total - pp.scale - pp.neighbors - pp.tiling - pp.extract_feature - - - -Embedding: `em` -------------------- - -.. module:: stlearn.em -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - em.run_pca - em.run_umap - em.run_ica - em.run_fa - em.run_diffmap - - -Spatial: `spatial` -------------------- - -.. module:: stlearn.spatial.clustering -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - spatial.clustering.localization - -.. module:: stlearn.spatial.trajectory -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - spatial.trajectory.pseudotime - spatial.trajectory.pseudotimespace_global - spatial.trajectory.pseudotimespace_local - spatial.trajectory.compare_transitions - spatial.trajectory.detect_transition_markers_clades - spatial.trajectory.detect_transition_markers_branches - spatial.trajectory.set_root - -.. module:: stlearn.spatial.morphology -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - spatial.morphology.adjust - -.. module:: stlearn.spatial.SME -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - spatial.SME.SME_impute0 - spatial.SME.pseudo_spot - spatial.SME.SME_normalize - -Tools: `tl` -------------------- - -.. module:: stlearn.tl.clustering -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - tl.clustering.kmeans - tl.clustering.louvain - - -.. module:: stlearn.tl.cci -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - tl.cci.load_lrs - tl.cci.grid - tl.cci.run - tl.cci.adj_pvals - tl.cci.run_lr_go - tl.cci.run_cci - -Plot: `pl` -------------------- - -.. module:: stlearn.pl -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - pl.QC_plot - pl.gene_plot - pl.gene_plot_interactive - pl.cluster_plot - pl.cluster_plot_interactive - pl.subcluster_plot - pl.subcluster_plot - pl.non_spatial_plot - pl.deconvolution_plot - pl.plot_mask - pl.lr_summary - pl.lr_diagnostics - pl.lr_n_spots - pl.lr_go - pl.lr_result_plot - pl.lr_plot - pl.cci_check - pl.ccinet_plot - pl.lr_chord_plot - pl.lr_cci_map - pl.cci_map - pl.lr_plot_interactive - pl.spatialcci_plot_interactive - -.. module:: stlearn.pl.trajectory -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - pl.trajectory.pseudotime_plot - pl.trajectory.local_plot - pl.trajectory.tree_plot - pl.trajectory.transition_markers_plot - pl.trajectory.DE_transition_plot - -Tools: `datasets` -------------------- - -.. module:: stlearn.datasets -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: . - - datasets.visium_sge() - datasets.xenium_sge() diff --git a/docs/conf.py b/docs/conf.py index d83827a8..4d08b253 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,216 +1,39 @@ -#!/usr/bin/env python -# -# stlearn documentation build configuration file, created by -# sphinx-quickstart on Fri Jun 9 13:47:02 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another -# directory, add these directories to sys.path here. If the directory is -# relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -# import os import sys - sys.path.insert(0, os.path.abspath("..")) import stlearn -# Setup files -import os - -if not os.path.isdir("./_static"): - url = "https://www.dropbox.com/s/3bb749fk68h0lwh/download.zip?dl=1" - os.system("wget " + url) - os.system("mv download.zip?dl=1 download.zip") - os.system("unzip download.zip") - -# -- General configuration --------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. +# Configuration file for the Sphinx documentation builder. # -# needs_sphinx = '1.0' +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.viewcode", - "recommonmark", - "sphinx.ext.napoleon", - "sphinx.ext.autosummary", - "nbsphinx", - "jupyter_sphinx", - "sphinx_gallery.load_style", -] - -# Generate the API documentation when building -autosummary_generate = True -autodoc_member_order = "bysource" -# autodoc_default_flags = ['members'] -napoleon_google_docstring = False -napoleon_numpy_docstring = True -napoleon_include_init_with_doc = False -napoleon_use_rtype = True # having a separate entry generally helps readability -napoleon_use_param = True -napoleon_custom_sections = [("Params", "Parameters")] -todo_include_todos = False -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = ".rst" - -# The master toctree document. -master_doc = "index" +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -# General information about the project. -project = "stLearn" -copyright = "2022-2025, Genomics and Machine Learning lab" -author = "Genomics and Machine Learning lab" -# The version info for the project you're documenting, acts as replacement -# for |version| and |release|, also used in various other places throughout -# the built documents. -# -# The short X.Y version. -version = stlearn.__version__ -# The full version, including alpha/beta/rc tags. +project = 'stLearn' +copyright = '2022-2025, Genomics and Machine Learning Lab' +author = 'Genomics and Machine Learning Lab' release = stlearn.__version__ +html_logo = "images/logo.png" -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# -- Options for HTML output ------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# - - -def setup(app): - app.add_css_file("css/theme_override.css") - - -html_theme = "sphinx_rtd_theme" -html_css_files = [ - "css/custom.css", -] -# Theme options are theme-specific and customize the look and feel of a -# theme further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - - -# -- Options for HTMLHelp output --------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = "stlearndoc" - - -# -- Options for LaTeX output ------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - "stlearn.tex", - "stLearn Documentation", - "Genomics and Machine Learning lab", - "manual", - ), +extensions = [ + 'nbsphinx', ] +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# -- Options for manual page output ------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "stlearn", "stLearn Documentation", [author], 1)] - - -# -- Options for Texinfo output ---------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "stlearn", - "stLearn Documentation", - author, - "stlearn", - "One line description of project.", - "Miscellaneous", - ), -] +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output +html_theme = 'furo' +html_static_path = ['_static'] -nbsphinx_thumbnails = { - "tutorials/stSME_clustering": "_static/img/thumbnail/sme.png", - "tutorials/stSME_comparison": "_static/img/thumbnail/com.png", - "tutorials/Pseudo-time-space-tutorial": "_static/img/thumbnail/psts.png", - "tutorials/stLearn-CCI": "_static/img/thumbnail/cci.png", - "tutorials/Read_MERFISH": "_static/img/thumbnail/mer.png", - "tutorials/Read_seqfish": "_static/img/thumbnail/seq.png", - "tutorials/Working-with-Old-Spatial-Transcriptomics-data": "_static/img/thumbnail/legacy.png", - "tutorials/Read_slideseq": "_static/img/thumbnail/slide.png", - "tutorials/ST_deconvolution_visualization": "_static/img/thumbnail/decon.png", - "tutorials/Interactive_plot": "_static/img/thumbnail/bokeh.gif", - "tutorials/Working_with_scanpy": "_static/img/thumbnail/scanpy.png", - "tutorials/Core_plots": "_static/img/thumbnail/core_plots.png", - "tutorials/Read_any_data": "_static/img/thumbnail/any.png", - "tutorials/Integration_multiple_datasets": "_static/img/thumbnail/integrate.png", - "tutorials/Xenium_PSTS": "_static/img/thumbnail/xenium_psts.png", - "tutorials/Xenium_CCI": "_static/img/thumbnail/xenium_cci.png", -} +# Configure nbsphinx +nbsphinx_execute = 'never' # Don't re-execute notebooks +nbsphinx_allow_errors = True # Allow notebooks with errors \ No newline at end of file diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..23808d9128e3118d8fb6daf83aabb5e670d77fa0 GIT binary patch literal 484189 zcmeFY^;eW%_%4hhpa`N;(hAZg-C%$qT|)TbeqcSr;+eho-S>50J6v5=o|urD5C;c`_>F?BCJqih@O+E>Ha_qd z3x(KR9Gu5EZ)9I+`=)Kp*tr_%`mbV8tAtOF>27_Anr8{mVN?7ryPacSZoJ~|m%Q&w zBADDiHSM8fn}qW!a`BhYWL}hU7-Xtv)v^%0zEv8%_T+y&L(FV%OAR!1xR=25 z@R9XJyvk=m*PVRYN5nsb|9|fhp4r+14S;9VJB~l27dzNo0u>g*=?936d&7osN|^j_ z1K-tsAMJHxnSVzE$35JMRnYYqZ?sZ|voDT8Db?e$g&GOW9%V}=sC5W69BfpvLud2DXNM6|HSS$nV$T@gT7577t2&~`rj*Zl zmgw;wC${GvP&Xge+j~nVCc=~k5KO?QyB$m(V$Z1GT3yG0P(h~u{(C&>&0$V29U;Z| z>WTv?7evSHNVV|M&1H_9&wsyGkkOP9`Mg?EvqOsTIrNSiiT&bgm%Y?sQ)93b#31u` z6se51+*BN4!pKVNmr&k+Q7G>LroLMczEb{RIu~a-<);U4F??Oa?=l;BsqyM1sG4@H zHJNe*-ER8trT%-sQh9fPm+;{$u^fK|HPAmV_p$H{)O?x6X_5->#Es}Da`2xIy!RFV zl>b`#&r@Pxk;+Vl^fW(#t?`tTx)G-ZX$Wl(*0J#S)7oq!m=JEMO9bwBuA|#y;)DaQ zlBtaT&0#yV7pqH#XEY@;eiw;eJ&h5&-eo!BWclE;wsut;2&Pg52VK1l?}RqR;pX0G zetsQi6>vpXHyInyVurJu#G*)aH|^hzzLDfFiB)nDjWu)D9uAgm(+w`x^Zm5>Q0)D+ z+5LF2yvaD+ozV5#6?`+s;S*0|!Q!M8;Rg<5%;c??1gIvdui0`sCNulEUwAhU@Qo}<)s|q(T^^x7<1GMm!o`kvEI~tL-@;B-% zDwmw_2sz0hwhN#-m7+%dupEvkPujj@zMtv;5*GVoQy;*Jl3!IzBGG_`?H>T55NW3< z5OhD{2HdBO6aFsc*hXm5tv~$9P2s!$l>BCSGnfifzb^kjZ*BAetECRghQ^Q1v4)u8 z!8!>q@T@pWT1GXW^6%C7Snd{j;MGXrRk|UJ8wzDG z5N#v2#9oBl|N0u%QnEIEnm{8_>h5usdzktjCc|Ja%RX3ifVMjz*<>5W=3W~5 z7+BW#iu9YNKV-?H2!woh^0`I49gY75C{PS6l^X382gr7#JE28lN27QFI3;3Quh9B4 zvs>4{GIyfajPl*t;qqbNsOuA1O#*j92AJV*RMvD0Ul6f-8Bqv z3MQ7WBu%i;a#Gy;sZ?KP_g}&YF=Xa z@yoN3tw7aRlp#=A$FPcsuQ}JnT4`wD%;96xlb!AaHDV+h@Qi!{z{-O@@nSWz(bnYO zM?7?fyVC}9(ALDJXI8Vcr0%5uHhK?G#$8Okk9>8YeRl%oNs-_pJzk!?rL*=@Izv(Y zHao%=U%N^wf1iedgI^-er|Z-7i|ZJ6N{IFvjtrcU0My9mJeYu@k2e7}8bu>{4(F(r zd20G2sUjH&c(K5T(y$6%)J*vkANA{U()m@hvvmyY+{&`g{-D zdVnyZ7~Kk-4ObLEF(|NJ8x9VpUVmvd&=|fV(8i5KPv^ql6S=;<{KVUmof9F97wI78 zpic%Q$Ty>^#HJT@4$)M|7ru`GUp4{<&TQ35nbur(oA$w{E4;%UG5xhthRx1%KIr}X zfS_VicQu~#{}Onmx~0CEz3VnB`~ox;w;{fd%e*+$_~i*?)=Spwyg3z@$xUR|4EV~{ zBOqq-#K9zn0(vnW)$r{%y0i!G)q#C&At|`IJu)nYSDmaOB@xwagk+=3Gy87fULKLz z+)zv6jLTw8o3IvjD`En{p@U*?$tzt|&q*Ef$bOzHBA1|-VT%{%RL$|% zx(zMiMr{i3oYU20MwIen>ZNS1-dIgBxCbK#eAfFDPUPQ5oVEFdAilW;R0kedgp_;X zH{S!CpD+I|NTFL-w>ro*tXIg&&s_LdH!%yz91nWD zmEDoMHbm&_=AG4AKxV<%-PNOV9-F>#DYOlHP!l_m?5EFG#`RAdYV~3_?2Bvl!XgL> z@oV@#zJtcec*0jA?Y&X`wZcGd&41Z1DiHE;6)|my`CHQnOl6-yuZ0|nb_*z^> zeP#!SYArHquNLyf5RYzOy`DWR6qNLtabx)CWif26p7nvV6xz!!gJU7XC8*gi{1MDgjf7&f0D$`oNM z7+lK2V4*~N1M>Kyf6Ux`karQ#!bXD|%?J9#UbMrmT^dd@d*@=$AQy>WJ_^kSX$}xz z*?7TKfR7zzV0z3$l5phEj@NZzXx@qvQ*%T@m!S871=>p_JXwLE0TXMGc{T3mp)y0i z9UJZF7t*L6tFGYvjLpH>c|}d!<8^`T{9FyM6QAr@n!GpLlK_6`bGFL-#VFji3wCm>HVt!nvvrrsxznbVA@9JQ8X;`F;uH9vj-&9TK1raZF zws=7aHq2m6dM%gdnrO%apgJh|c6Qz~KJzr!n4ze2J%$S=1B0@YZku_RmlrdN{ujOg0aNHU?GRzeJoy|a#F<_8hpZtng%3FA5BV@=^ zAxwQGf$C(t8)weaGL%V0Sz}t+-`UqkKboJjH9-12_LD?(agqCHkMt)3zXaM+Qnz%b zazkRrZE7J$cVwYmVGI;)%M?&Lgp63E<_q`=ZH9Wiw7Hxbwt7o3ayTgdYS|E#ob<} z%!ux6RBd+@gcu}W9kCN!YwMxkir)+3;Au(>IfsT-O&vSZA3b`tmv zm#NqElF|eO>Ra+_Y1`5vDG}gZZnd5Mcpd7;oa!E!N_RYyWjO~;p3Wy^s!!YZ_qL@p z&5iuBGzInu^hO=O1bmctQXgeoCd0~=UEWoBlO=^;x$22~^&-h*4i7$DDh3`WlUn3q zFuXEQ%%ev!m$Lz?d}bxjE_u_WpBhfMDk#(bjCpqDiy+@CHKvwyV}W<8GqJCJkW)4NbY{@<#>?pClsoMHHKVY9N2;9u!kcOFh$TRkU2j&Q+4+;oVah?H@kerQzYrB5 zP^Jj)zpClL7pucAEosVswj{jG5X^R-?qkUymEB$C0O$3VXt?LOP>_=u!qXRi{^kk& z^0b%@L7RO)BD8z&#~}vw=lAb=`Y}P!ahgo#a06l56=j=l8Z9dD1-|cb3=nFc;Yn3I zob=*s^^w}WNGz&6aQlT%Q;tyEO{)$POv~B(_nM30_ zDNyaSr;V5IkV588jCGB)aL>6$6A~So^OkoqMWuJh$Vzvd^%+9A`p5|S5$}f!|0cUB zGIGsWg>L_ICnU*=$Pw>C%EQFU?dOPwilUj{67W^}71SCv1klJd6-fQel5 z`t(o%2n4?f6o-UjoQ)!R_;9|1_R-MTc*wpDhtmY?dC{NvE{5BnqRJgEWP0R_O93`bk%k1Qv!CRq@0K*K@Uc3G7><7rT6}l-i$?M5r;6al)s*@j37&?m z=lf{%QILkOU7AwE{nAmH0F{Eh`%S)Xe2cc$<#y%a)DcGY%VbPjLqp39g0V3?Mlm+C z4L^tSGXa<3&xL=&6%(nBV#JlK=*D6F!(qG_s~=_L-!{tGN?MLb?9w{^P{sGHCo=X`>8J{rZo1hRoGEG2QW1H- zc(Y+I0y3HLy{pE1FgwsXx~2XIKpp&IOW5148_9X-^u!llaR~^#lb~l@r6M~z7t(|> zOrhECM+jNvILe%FMarBU_3TjhEI#1nAO9X6N##Tb@|>z3V5fM*GHYuoJ2s!CC8_=t zUfvbi75}KaEZlw=`%92lm^2d;LA2NZ7&nb*&y!DPjwL{?;%4K@kHuKOsFMsGM>7tB z*Of=g$C>e6OIpb4OJ^DKdmi-WcjHnvE*eAKQOdRrV2Q>wAcEz%Qh}h_bf9pZ)+>Eh6C#rJ6

N=j`1YWdJmHVkp=uum5cp8?y1ECM;bv^5jzBw1yd|sLHaU5ItBZom6N#GOh*{v;F z+I7)R{hs^RwpNQ9bsB98N0HjRO9Xm5s!i!)bx^}Q<|F-vf```V`iAr9mYRY>a#OwT z(*sz$k=9*OsYp_meQAA??O6ICZ3txY`+i=>^mGJDY^wk1ZvUDPs7kVV6!EsV8%K?S zzm+ofMl)Xlo3!gOvrgz7LjQ&{;&aub_ymg}C-->d6A>|29RSO!z0@uR;*EZkH?4>H zmzdz`CZG~CfvZK zZX7H>f2)?rmt%tII@Czs~kgzXwkX6fyW5 zuXWAfo^>s_HStl74S2%Vh(50nJI>x}S`Uc@YW_F4HBWE@ExK_`Z&1y%^L?z3EpaKo zdBM|9xF_rRf+>BM{lU~*rb|f?8D!boMJ-1vNdq8c2z*6jfXL|g#VH|Q+{FE0YGC3r zuVmX^V=MOQiKI~0$K)FT8d3Rdv=CEl0^0SL2F_5)?O-22Iw~cF?z$@=6HU%#s+BN) zlJAI-H8UD()BLD<&) ztwNVXz#;lDjpC2?vmfSsaw2w#o$lDkN#j8Y(llpTeBTY=I8;>gJpw9OeH*(Lc&Emq zoUJudkHN6#79wEO@*@v9?8kBK(c|x%-rdZ7OT3~0$kF7)X)6*yK{ytQ84$$Z9_{iSDY?&+%JMEMti2PdPB znCC%FTki>&ch7PI8LP|2ttmaBC0*?Jjia?w7`+~J%g4|1N9fba#dT4!)&@so@#SSlCss&*HDv zLwUQnG&oa2?fEz5OnQm>tezdUnvq%$y6cur6wt@J{70YGuPWdd0A2?j9%=Q{Z_ZL& z8chjU`feYYq5D|ldpfIWu70`x4HqYC+}V5w4snrSd2D2n3|C2$>haa(rU8S^P1;~_ zv0UWOg$4=&9dqgNl%$cag^5mZ*HWxd@?}pukPAk)2dyvbJct3I11|A*=ZR%ND+dCV zocrfJ?bE+8dsQiUb<>9RQFH&+Gupai=mGt7nZ&ZDnK#+JjF67-$Flx=G>-vyr4?K> zM7k5bGV>z^P^q`U`U0rp<`o~ZfJ-lss&diZ2)kVH{#_(xgO!?%Z^yymVM?aCk9RMF zu{_bAua@9XR$#6?A!xUe*3U(1^6x^>ByN|gnm!J0;{&(ZB>fm6;aKh1x5=24o#Lv& z(G=wcD|h})H`M~HoL}1w%-u0*HRtE}r)NO8kp>Ro1V>dqjHS{Eg~jRL*g3+ld^WS& zp1*MSmF}FT#?d{W-58}X_HUHmork;QO`Yn>Q5@IdFzdk_b^p_8%R%;!5FY>y{8SE< z*tO2Ei{W87qY`Pter+kx!Lq6<@V0QB_j_U5t}10}Tz=L-tvjr_V&hnMukH+2Zw-p) z>6*qLRb=+~ZwtxIhJ*xleDvZU2YLe-MZq}$gPl;4bvrv69fd(Bs#iaMn|iYc8Z%1q zze_~Kme6tf1jvNS{2)P@Z&j%NzZzknn%@LM{D!hd;NC@FDUO=vHWb!V?L=(K%;A*m zMurbZ@Saj z(4d=U09PKGKeUsRIei{Jbg0TB-)5#yx*6b9TB4gDM`<r)?KRLe`D^2KQ~?M226PG#Tb*Cg4msEZZlPo4 zb)W(P@SOxOojdzJo>CfH9WY&S`ZV&6g)7Ad{HV zVVq$bonx5rgP9rb!A+RbZiYK!@SmUc!86FFAI_9KZ2OCm5iJ*RN$ZnV0tT*Q-de*K zZ!Mpeq56!pV%J}=qObsP{ML2UB3#8;Qn*|9TS4=Kg45xt)cC)8c?s7x@GMZNBM1NK`_;Wy-MPT_V@Q4z!Y z(4biG82%0w09%_~R6n~{0CNdKx#Cisn}4f;{l^6`Z7i8gAGA93s|GURZZy%}NX(63 z*5T#4#)TU?p@rRm{~!Knyt*0X9rS}4{KWW-w|SLJmCNO9>CsGkT1yI@n!|j5q^yC{ z{cCfh%za?$p7!XIo*pwXfHE|tJ4v`bJ30?m8osnicj%pLFy>&_pBo2*`#cVd4m$}n z#}yPghnEV~`chJrF!#sW40iXQ{xs5plM<4R&-5nv_&@m?4Q<{K`+l|&r1kXnh_(Za zHL3){%0kxc#+2JYsEmzN_lp#83vXb;y->9bbs?gfH%+419NGF!t`q7K1JoN4OIq(K z(>f@f1;>JZg+JjcZs>~d%5-L~3P{iNTRL1v{;Py|!U{ zQG73|y=vw));g|s+6=j0@iO@BbD+7Dn+q{L?mpAmO%1m@Zxe`=BH-^9=2H|BqVl`x zp(1DfIsITQQ<2TlJoW6%Qa;z`GNtp+Lz`bR`iSxG@f)cfcy==P29Xt14)$AzEonfq6v4Grx8V)cht7?2i<9w-+HQs3_kp8AJW4kCv_NpulG3uJ;v+u#)&AS&D^8X8J z=W3F8KJ($)oRo&X@CpI`{&P_r?KQyLz(-1O&|JNZZIXZ$Z%^qu+BOzIdGuYH+0Tzo zEE!~2r`jJ5aFtb>E*kIk(5GlEuMK^ryhG$LNMFn*P*O1<1kxKTdI#T5k?^uFk42As z*WsL#(wR>*Hc8Pgf9uEk(}zO|loY-=Kk|JGf#7XYT{k~7)piU~XnyN<)|LJlD6nn< z<>?FjT0ysiye;3a9(ZM{ncc%$u_@B!fY+hl;Yx@L4H*==XtRL)jzAsc`(bz39VO zA;G7cXsbfoEL{_~h?}YhODC$F$0epc{+C|*EHpB0jxb$&``)#Uhc1FGhwd{9QhhiC*#+5NHp1vUw~a z1S~^C!ie?`T?6oI*=v&v5I_jyt-u{pZ5Nt$)2>cON{iGn^2_Xy%|NCy+&lKI4M9^{ z)Ghnj6}f3w)%`^Gp* zi^#Wmcf;K5ZIRaCAkmu7BB}WAYU37N!{W>4e(aeI_$wQ~RC*_Y(N>7Y%+*&eFmDie z^QL8J+>H!pX9NyI^M%sK-cuY0%*Qf0?mFEfNwV*p5@@Y?Q-Hy3b&oo2@HM_u4fdE@go-&>_+QF z3=XhA0QVUhp?CNWwSxlG#{nc=z%-FzNppOy7yo#DRRmk@`>E0w+ z*)B(XHS+Qs^wBZZ0*QP!AVS* zy-$dg49Q4!`uR68Y42{Ugq3{=(9<1d`n}kezcNUW%R>0OSpf}4?UyN+NnKd2D;MwGPL2kVzCCsit@4Dcd{sb#_dvAKKOXL^s%Z4ua^1lD_ zk-y;CP$683-cORATw7P7fdrB%TKe26O3}NzPgeti#yz*fG)8JcR2EM{^tH=8G4ctG}MDbLU)$I$)cPf&}ztlIiP){ za^*HAHG*t{mrctN6#lc=l!ksb#@*JC+393->e(wpe6+8F#ON}+St z*EDMCWB(5J2`cKXW;J%}U%w_}v@!!8B{Y3v2o3DnwpF%ZNFV9Iu7`}u+9XPr*H+Z` zxl9pq1*w{^6Qj4CsMEXC@;jqyx=_<+k_Rp<1>KS+qW+t11)Z#ESgB(qkeq@)P8y{D zdWpO|DR!J|g7}_hN&XB~+->Lzmqt@Kr{5+R?iN$m-gZPapRHmWH-zHZzV<!jFPET+Ru3 z9Hlgdwyz(~Y=QqQ80IT!s|-_`y|HAQ37GmI*pasW%OjyUKZqA$(2Lw&rsXtUqh}*623R4G>Fy!>gksv_YYY9!*N*0&7~96J z(q(qriB>oy9(^Y#>^VNSO0H!a7BI8tzziXHJN@;^i{+e?uPCi^#%a3eN;e!CfkDkG zkkZ3xx>m|1OK}Fjt&q>hV&0(6CoaTHiH!V>fe-?J#nYEYM+=e=3fxCQ=60ddQpMbcK9HQ%T*~CD^Wz->s=GJrNxu`Q=cX;`?UEYRT#{ zS<#%+tx|kx@$A@|XuFGC(v;2Z;g>Eggf&ia>0cy*h||hf)wPymH+Laf5K#6;`et)M z>GSff5?w9(kp{aAzt=h%)MtvyQ*em@0I*#?Wc*=m<8k1ta!W@k(i3~B8PHHlFd1Lu zl0Z$Jw_#t{CChJ7ldDnqpb})7e5ekTW9vT;Aamz5%Y=pDT@9yF)W%8Lj=`F8plv@_ zZO$J~ULnWWV(ak-EX(};fi7ov6@@%kR6vzd3$lkRq*`RV()+k5X-jYQ#}WVtm9m93 zI*60va1o86l)+U7Ck;VQMt!j^eM^6?-li63RldNb3@-TQbdI#DzIQ#`Fv{8;0c#r} zAvq8_XkZ=dpwaL65NswH6`dCJqSuZ)NYpTp^v(bY*!E%sm2!Gm*{PcGXQV?3(H_}S z+RsjwWP+bygOSNp|y`T$D5*-NfZ zN-8!}I^k^p?H+9m$drq-vUY!(W3QXz9-F#OZu{{z!p|)N@9td(=?sq`)`w`vx!4gu zK@El=l&R)Xv`t!ks6ytuZ%!#|7}*7+Sz)o8v}$k)k*mZ5j}$$|gPYkL04wkN`n;Cj zP@1ky;M$@C+V4GwgYVzxH>l%7XN&gZdZ(25n^A8RuS-mjSG~6TS{A zQYWFIAk7t(&(p!;W9?`nphuCcnf@|Z#z(}0ZNXBy*cVi6>clM?S#S%f3&$$NE3fpnBynGASNH3B^w;l4smt84pWS-fRq(tG>Ct4QBulN#i+1rvT3pq);3Y&`6h_ zazba5hf$841_+!Q$zULj&2ia;rmhZj)5qPA_S{Dy2rmL3WKg-Ae+KwV9r2ZS0OVVn zOEMRjY(&Vpds5Dd1DwJLgxc~5LR|#jFR`uSzuP$a8=zC;JO&SaQ3H->cV>`SF)5i$ ze=D^E1LBWWH}}>F*SA|cZ#izm-( zVyAd~#P&Af$S8ZgJ;t*9?fjOg?st02L<4{ty|6SC|yBC)3 znoPSPMLhyGAh=CUL`rJqcEg+|<~<3Ux-oMf%Il0OFHS#<3#{}Q*E zNzzNh@W*l&l|ogwC}?|+V!U=&ZssXqC$aaoFD9|`y-R9%Qh)jP=#jk@5pC_E)s07= zPWwyoO)l%;l)vNr#{Q(9PK;D`%82DRkDH7mu{$9n#UcAJp^LGG6!Cx;Lp{ZnQEkhc z(KC`zU(3#L3xy+Kw0jAExt{7;m$tXI)ZVG{l4Oz-D{lF*4L%!Us#tykA2ni^I{3OZ zrc?aG^_4|G8(7}k(6D##2dx~L@1X{gfJA&>^Q;#^r$x&zMKkudN&P!w6lFZ<#*wd2 zCUQ0X;n^qBBfR8e{`p-8g3#46*~wuKO{4k>9T?s(*ia4ek#3k^C}6^#wJpOJqAeiE za}~dXAATVPKmNvnc}`#`0Ey*vamEZU?o$?R$I1+b#X8dWG5kJ^O0^s87!0=4c8fNA3NXtvWHgK%!ZHsxPx(h0I+>j ztqGRaqDasMEnk3v+b$7jtH@73v++HpR z_Jr=ZA3|sQekvispp{ILlC$+L(8&=KN1YA0F*{1-_DP1{3xI77NFlk1nlmY|ePG7% zsk<%mp2N`{M+bYwvqLTR`!UA4tPOR@W`TizP(HhLSG+dYv~k=kl=#q+3XbDRqxF3cPW z=~K|mR)hm{=g1x$UVtb1qn3vkQ4GlBk_un~c>t)=|B(okboGp`3tb?i46Sdfq1fgA z_UiB4d|sHvAU|h)fxE?P7PHcZ$43?w2vo+Kl zen)7+u>7N&eLttOdBVfH529#gviG$Lh4%V#A=t?WLf9ULqO9i5b+ed1Rc6hW@5jGy zALKj9zYrw`A5JS?-qA6qCiC%;V6ZlyG1A~0_AndgK}JPwB2HV5{6_IK9jZ#jz{3|4 zWOoHc*&G4`-MW76{jfZvNqTcZbR_25%=z*O=I&kcVe!E>YGuCeXLw|4uZj;^NDYE6 z3a}q#w2RfP91J=`kDmM`A@R2so()2__d-&2QQyhuks_}9D2}ogUKb_-m5`)wlT9h9 zYlzgVXLY@eHMbL7bltX10P=P_7A$dO{_};JQGgV?EVg|q zk~t!oN*@3q#Y0T^C5Y*LTahnfh$Ozpur9~7=|5w!CA4U?L48o$IF2*SNBfs)+58kz5j zmvR)kq1fC{FnTLI-%lTHJ_(5+%c|wEj4*Hoz=2W;xxlqS$=Y2ubGJNWr%Y~^D8M43 zZLXrG)Z`SeAq3F_mOB9R|A|XVA>PY)#0O`9fAqlyB`76W+7tF-=5)D|r{$m-AL?oA z?%Lh+3e3Ar`y)X|iE^?!Fo7A|mFxRS$I1n-FAB`68dOjmnK*hGmsq)7(_Gd$cU2uR zIkn7bFqao%04)}zAf!L(JbcMjB|OTl^AcNy-Ek9r!zXc5Ts*Is9RsG-7qUhXV9xJUCj;jdDPe53?$)ft;oXuDuLMhN$vs`^r8%M7NKXon|N8C5V2g^kDF-A_2bOrDMtR6#LYc@Y=5FR`%O7+Pqf6TiWMq zxrB~W1v9e)>q}-V9stf}43OrGosTGy2T_dxw+Ntlo@NgdyeFSe<^$MVeu+BGmv5!% zXX$#WNkruakmah}$|OZudD304YrQ@e5(r7m<{Oh>%K>BQZ3NoJla>~_E`WX?L$SZs z=&yRAeiRVG1Pi_)$mu0z(!x?N;}%>u!=YUITXrtgf6#cu26pvw_i}Ida9s(u(c8da zd+J|E!BUtusLz96+D?Y|jvj0;MIYb3R6~gGY{?t+G~sHK{Wgc50i@Q-4!wmp;A5nv zaej|nxb!k|;}fiS)=a-3+=V*HuK07~9pb5kv+XL@;tg)YkS*}WRI^^Ej*@ z4JV0Fpvn?+E@bN50sc~2ziI`(k&=gYM+UcWRamglsydghVymjyHLCqci^J68t>t0t z@Mr8HFNzOrEOmhL1*A7y1a~?=Kr5f$eA4ktD?-lNk~N2eYbpcCG-{+$nc|msHoSwl zVL-&|KkyBV=#LU(*wTK(3-Ia5PBy$lhLLq^_z6~GUG~9O+{#DAj#HOV(H+NU5?TI<0(A~X3L03D$T0Hs}Z=vOTcD$!0Pw(ye*Zql~j9;<0k2COV91e4&;V>GgL znzy*{L!f6{KmXZ}Z-$whIgDXVBaRan|Xzo=2OGbgfhB}5A=ua zRa0>ahD<9B<$Zm_SF|jX7Qu@JDI~#GUPk3{Rg7v*We*Dye-5Mqa9{fEJg=zH!@VMi zq9N0gcI=G|slVo?jDHH```t#~6?Pju-#Hrqod_KQaDg9{FXTWH=$#223+mI<^yJt& zHx&{S?Kd-0brKWY=KiN}ad(Dr*68qHWZql^{~ptn_R*(VV*ZxH^R%lW`zTUVIJp8#(Pw zD?uFn1!1hJFINT-&f!MMA0iRFw%&m+NrNuM&ksbr7RoGAGSdDW?tQ8J@pwvkuW%s> zk_@eCqDSqce{Rd0;k_sPP9_oc5${Fe>qX+=e72ZR zrdgVG1&gs?wvv;-!ZzjvCA`@*l4c8#q)mln#A?Os^9)c--D+Lg_6iy#lVj^sfOkUP{pO_mNewLvXHzkexZ$=jXylWqFVY=FNql}cZbr^O5;Aw-;1@y&Tz429&q-nDRJ{`r;%}E@3!|cGIH=lac_w zkvNXh>Il13HG4cVmh;vr>ZVXU`r}J91w}zHS~F%#h&(ejuAs1HYqOlXq1hzt@|+xW zmU2^Kh+afSv*Y;H8*+XNJU;z~>K-UGA?U1C86AR_~s z&*pTopOTtGY!+110A=2;f3O|isfLhLt#>)`rof1sc04z@XKAVxQOUhAM!2`R3%?oo zUioKFpy2d*I(HB)jaBfm3bD~|-(5T49u}|Zi}f+-ES39r2Suf{Cczg~o6%CF*Qs2W zKbHRQl@OJ44``C;K4pT$0H=t>Lmo+q!s4GVjQw_*(ocsXHyT_uV%OIVhvRyV>1$`) zb4~mt5uXREL7M7WIle~OZ`HH&uka0jx@TKp+9-*7N%u-t7d~rW9+Gm%oQ4kjJRX_O zo39r4yO1vY@FT3Y1o?(4E+jiDfB`obY)O<7Ad{MgW{=poL5IJy zI~b>RBk&L>Uk3|i0}mo^^>yP6((<ZW0;JsFnM1(;nX%QT&&iKp9{(Cy|<&A1=o}-jBW2*;BS;IKf>g zF5%F$gJ(lN1_0O~B!evZ1%Hj`6$dhlT`)v93cPFwt5dm#_G?xD-Kex}=+kLWP72+q zF65_fdj_<{$a1$MliEDB;0rCUOtF%l><7%h%xr`LAusT1bS6`K?!a_!^l4f>ew@i1 z->vB+2+6S>L25j%P@Jr8nQ)4+D4mM#BD_U5sZ-e71};9#kb?Mc#V8(cC5UMImOM50 z3(}7wQ|$DI6$9N*NXEw5e_Vjl?Cg=uZdrxoH-4~jzaW*^?&#N6%nw!hY_)FPgp)8N zfKsonSOEIHx?c8TiWE&SSE0*0r2aF(!+birgm4IYdCZvuAPoR?6<{6;6nD<%zaluM z&`Y73f5${YX@HdYCHw~hK79(B^HL$3OyN=zDo_vJ7eY-7GF(_tqoHwrE!qLQK1MA9 z%>OP(j#Jg|r9?QK)VeR`0rXxQFS~ly1@nb3=CMW5mvbSxvjUW!m%~~qbksVGv9+TC zkM)n8I>J88v9_#eB##&JXX=uFgHTeCeeJLuL#EzYS{c-hrnudoWUi!=5ZSAE-E{`c z2glC_fJr%*xw%|(K2@hwLD>fzb;H3jCRmN+o=rG^aibA{=axng7S|g{XR0M&I*=%j zh+DBA@oW~S=xO43j18*XT~QeUNfvX0fJxIyhL)aN zvyAdkhoB&$Jc`3Iw2*{qJ`%VwAqwD-!3P#ql4+gie<-SJZ(axhQhfWrvo=fL{lWie z^EsXFV?k^9s0wjf;>uk}BgPD&P5sZM%u^`Ujx%(om_l|Cl)-kR-}J3gqTkf6UA#Nv zG5IqvY~y)WC^I~|#G00ld#qWh4sqyEc55}+kWgPdDE_mlk<1mJnvQ$-AX0)L!JGK0 zd*!pNsQ|WH7laA%kbPt!=sXHm>?>bFNV7p!1nKQj$j&QuQ|TCZdUbjV0k5SodGJh5 zelIGD=^`VH*y1HmGFMIR_#z%Rm2orkVDl?T_i4bC@OrvWUU>Ld2I$N**RBPa8#k1Q@!ek{E(+XF+t}yuG z>1ND-X9S@ohrmP3(8 z?xG>=F*VEu#*m<+5mF927pIR`6}Y4p5&s(Z%AdA`k>`TeNRRIxxru3!t9+nW8#&c&#)UpAnf$>VQcwF`0d zaQ515f=ftHE~P5RJ|^%X`$Du=`55;Ch}dfB$OS>EK)G$Ti@q}d+f+XZ_m=-7v+|Zs z*uNSi40(~B9JajJ#fZ8_b)EefCv;=@FOXTC*ATz5?9ok^ke+7)iKVC6M!-8(#SuNm+|1|kFz%3s&~+X zAx?%uckiN8t=pBP2o`I|adiAk+citU7#B0I_+FpN;;Zo%sH|Xt19m(_)6rhM=K%{! zXD;mRbE`6L`->eH$l|^{W$FWwkv{aQ55=x7jQzZ`s1z(+zjUxQX`nwapc)7U8UHx- zQFN>=hJwhy1G6b8lvoO(sC?_&@lL##OGQqva%^k(@tSlta3zh3(-J=-lAI6K1XDZK zocnAfi+|b~bEEktKnAwvZ!H->knxtJw%EJjoz6M5hbsa+RQpxd2J&{7!@JRP(-2ytHfP%@r#`?eCnz0)-hsna!v* z%)aZbw=LSgpDMIv#jOGf+PDLv4VLX_@P=HpUK+~iu-&OK)jGQRY#DUAut>>!MEfjO z%HIB4OOZnPJH6M!5mQl7@IU%&d*@cWbG(DmbnsWnPAQH#Qe1t7y{q#4$F$bEVKZYa z4xZW^JSNjtF}Z-r0YhF+3p`)6GQKaDWLZSPD?_I>{c@nKm*H$3&&K|8fY=8_nDqO@6QInhy7O7cm~(eIvqVlCf4ZK&aaHZEjb$c(8E8*T^a%|Br&~Ren0|gMjpM zf#8;RqaY63Noc1_po$d%gWlI;cAjsAk84Gxhc_QnYaqieQ`Q?71l?5#0>s9O>J_t{ zi0v=ZcZ<*q1R#H`6gqhnptlP>sYrkopK9kfT6?vGQ$WfLJlzxV5WnzPTd_g;IgwVxBIqu}mhtV%GIQy;Y#Ql{?` zpQFz&HSsBG!CZjqL1k1a`X3LvKi3!T_kjER!*#dRYF|f^y{O^T#ksL18Ai3&?mf*z zE@$Us+6|X!`wQBbIoa)3s@82*tuuq{pWs)!9d{l ziBez|&JunJ2PRqdh=DETrqjcm=xSi|mrZJ}uW?0tuB2ko1Qc!SvH^VvoW9FKAOqwGaI*I z<-Cy2zL3GL6{U8F1|pxfzFK=z|g|IJrkCh=9R6 z5FDJz-I!y=CF6eIL8+hQzb1nm)4S#-OsP%u=Ym$)1MA?r@3?IoIYri4N}ArRZ{b8L z#j%>E*X))=FakEy*)T0xx_6C=0yA`Gu3ymP05Pl3Yf`3A^v2naP-k6w+wHuj?>5kl zF3R{*9=M)c?zbu_{FYqb-J~S;GE0T!+_{ELZlNxyb$+ah| zDiJ_H@J0X-vWT%@^7=q!y(Ze|L*}MuQ9?=R0QNVBN)b2xtSu0;Fn}?0B5bw6AwS1cMpZ$MAZm;0ti8r6pScxqBk&= zX$%kt{5)hyDSlLOac@+L<1O^&tuC=HSKHlRp3IKoNDC%8jw-`oD?Ia>6!{&!j?RsxBX~X&S`TK={)izw?4(^qrk)5S~80?k=6}oe>vh< zXr!2N4J7NXa!K{O`*LK3hpCKsUy1X^_pqsXSrQRCOA8x3wZiy+puaJ;OQ}ka8AEv zVA96688sN`V_s#4c3WMZAx@A|`~nO6Wtk0q_3$bgLs=Y!ksdYKP>M6xKKuYz)K{)v z!0Gm_e{`NrNR5WiWYIiuN&TA^GEXHGe&oxKWDH#QJyNRv!CxORz2W8T^dT;s(9$h2 z9o6SfXFr_kX_>N#P$Zs^iy=A?J!}9~+gDz7rb?&o|0Jcy+6we3-XrQvjK9UJ?P;^p zcQFs;{luxQ+Kaz`5p`i( z{kG+Mypwlf!_$pF>G(J4%gcf$ zoHUp~kp3HbVRop_kE3!u;@ABaHRY*miEbqSh{qJ1Acn5$noC9YgB;TO~z-IN-3O z-u1sLU-{Ev^zyk$k7!rS4+xnv*F4_ohX-w^4ppr`Unup}9<+5nzmb36*GmM_@a_fn zKDd8SXYYp-;ukof=4-s>XwvNZTP5)ZyqLAP7{yFwr>JN)Xk60uxnnziJNq4oJ;tdh zE=&6bS#;+-7%Bq^V*NzDg5*5{>Fu;B0iLYx3_~AS3oQmUIa*Xl%8l%^MjIHweKH>N zFoXIjcLntL_}i22Wv+Vx&WE*p86V-Az534Nx4u$%31wsmKb19}C%u~Ods@`JH7ICW zGF$yIbwo!5*c|t@3=A-QDIA_*ciF^E9&4_u@cIUp+mrl(Q@qn3K!L0P3gnnHUzc+g z_C;X-OOZ;{wyg)m5mH(5^3ZXy-OtMeks9*f(vQyMPJtL3xz#=Xr^=unkJ0ttL^589 z-c<0Bs>?IaQ}&)q{{PE}DW1g#@n?3bI9xf>+B#m;QgLk6ZOOw@fw-1JErm>k!%!?V ztw`+*EsM`Z(LV7^gf$hc{KmF;5m9*+OvV^h`>gyOj}qnna~r3J3s^&2O1)4{8rEkz z5;qV$$$J5PmEl#p@?2j{-XwEaOMKD(kz<`M-)hx7+Dcb*BGiZDEovM?Ki))lpWkYD zB|fJ*JcVVEWZq+rX>RlWH}KLWftRm=FOK?i5a|*(S8>=l|6BOXrj!Ea)ivS}iKhKV zoA`xuM0+_SrmiLR?t@omBaEEPVgQ=d#4Gkt2(D>i)zoRN7?*mevK$VAew)r}`hYDq5N} zab0;z(sO_OpGH6D!`XEtcKs2r)_LPjc{eL%HoWtQWPM3##Fqw(vTH0ltU`5jb%@^d z`p!aFVnOE6`1sR+V~T%Vy=f?KV|>O>K~TAPz?;7A3bo>6m?BM-W}V>u^L`%h*SPvS zGTYyjtix1vF%OWqtn4s^Nz{RJzYqMRJrU;W_S^@^{I*fh5J zV@U^6Fz6g(Ux_jj)+*xh>h#8ty1%WqEci@iUY)trIgLjkXc;yNa}(B-1CgU6Z+*m8 z!p#Km{oJ)=JsaXBnHc^jgw)-l;H{bzHs>@f?wOr zmR%3io}d=ZRf8mO+!b&bnk_Wq$E&@pr&3B`DPq!m(RIeS7iJsw8`wtH-hHy_8fZr$i@?K6^UGnHShkS4N@$8*SJjF@arzSy6 zU2T0g#eV_N)arp4ZqWyHW8o*qP0_ZGht+3CV-J#5?d~|V)tR}c99tzR*l1YrFthr? zZ%6$;3Fy9ZbmibEvxL3^Lc~`Yx^S4ZN3&+gE~nFtYkvPR*FA&aw*N?-v+cljovhHe-j{=zv}5wEiJboGC*Pnt^|=x=QEJy=wX zL3?G@>#h0ssBLGTLVLeqOp3 zm-u8cJgM{k$>CjvRB!3ZHSX)BV5Hf2576_6T#rez=*j2^@o3s4B`DLkx5uMO=6bK` zA+7fpH1kf7<4&5Ml+Kd0N$u$vtAs+kD z?8E)H$}6s6H+^<}*G5Yr+IdJ)S0S@%kUp~ovp~5%C#THPMvD1mW@jXB{=IC6$Hg~68GC-W0 z|Gf2n$*}{<2f#Oxz0z^sF8emcB|OO(rdKt1ZpH%Mn8Haj^%I5lZwx)m1h!)oL=32? z=`BKL{aqPlh;qlv`QC526^4Fy#K^q)zb3sY5jRNRwlpyAwVDau?k$nOiyce(LTcI* zwG)asP)vDB5Lc7@`2O`yHWDlzB2VzgMTnIIeG3a+P_#sNk1h*eVvif*q&vOrOP17m zMLXSbO9k1yJAo_raqGB-Bo1&}A;BS9&1`ddlkSi>*UmwrgfDB^hn6#7AI zA1uFVO@b8NgVigC*iUiX)LU2@aNKUhtUUF0fb0Yg>dGmsUo@o#?=-rdMsViKbHx*# z6_&~$k18THZ+i+oHlFTwo*F0mwqxkMOkf#(`QcP*;t#pQWhk%<7?ZR0zgOr#X7{7M zQd);Oim2#dx6=>n6IVwh(c=XTC^@PAHiH#1o<&>Hi=0AVs6M4hk6d!8&Ha z4FCK!!uuoy^CL|#+3^U6Ta~FTb{f(U83r7(x^3+Kr1!;gPjqx?YmG{dXk^Au3URd* z7KVNK-edeN;0Z8~kWM6it7RaY7_}$K8)cK^A?H!qVK#;C(g6oj%Up17u{C`XH|Y=? zr?KdIygJ*l*MoQwS^e>tp-2H!_&&rL#?ELLZ5UiUl&`I5_sc!H@ijfF^6r?oC^H5E zRy&a7bw}1W^TXV8XL-exPnL?Z?~Nhk48KdvEgEa^8tm?K@oSgL4lA&Rm_?0@SAI31KGd4Ek2BB;US$f7AUFd$owK?0P3Zf!k41EwQ z~7q~|xL->tk0AvuD4V$jq< zCNSlCdvZfB6+avHYr7ZJ_D3V@P$E2z!gcfh0+fzULG#c{sjqC_JI4KQws4&H*pwV) zBc9}yeq9%S>?J*|?|dYWuJ*12cf5a1F%PrQid=Glzn_ZButR&F#+*R*oYbGQz2tR{ zy09U+=Ghf3|KntJ&w^%tCTcusL_FH)6 zRiXwp%}i6Rild6Rjw`V}cDXZ8grMD;B8r<9nM)=SEC(eSI3HjPufcT_?h^OyS%*#P zec|85ljR+1WVq6+8EOWWS!cmh&PbaXa}d`~*BkeG8qWjpvqJ${ZHZV+1md1&CGi&yzsQtBM(MjPUM6m+s6zj|lE2%mwG+== zE~8IDO}o$8efCRoL)bheyJJjP{W${ZKCq9PVSK=TXkesr3_|V&Lf{O|Bz~w>el0|~ zjhG^bUj{1QOI~4&7GCFR@U4lPY7v>A=REB6+(JAdoW%b?hTb8=pmru&YzsKCdRs)V z-#vxK#%|*SNv#{bH&zoYnn*f~UYXfjc$^uDq!8j1kNqZ9-WYT~Ij(Dfh`wyco!C%#eXIfGB6h^DKbJ+f(^A=zi^U$li`!7ed;gXk16on9zLfNM z>?_x{WV;jpF74dIQYb7o!z$b~XYbesB`m1ER~@)gjZIyOv`ByjM*pa0MhE}JbN*05 z)}1#%%{7H;w!Nc@ltqAFnlEOk3E_aimJi(u4qoLztb_pYFEg_ZOjv$jXhokLG|~!Q zU|XOvrS0O&nAsZKuy+saUXj{mJ~ntVklgd#x|^jmFLWa|EnV7E=uLaCq}HIM5$-GO zOz>Ybw$(ecbC$=EL!POT*r`_m5CJqe=eIcx#%e4LucFGmsx~NAsuyNPbu0L3W=O2J zuV>6RgPt*ck$*Dw_Abgy7#P81&zmSN^} z>uC^*gBMkv915aHx_vACL^l!MC&(YXDq(j$J)qU#%_oRVDl4oY809F z<@|uEo{*((?VF|i_(0krr_1blGF?P26lpNAAeERX@oQ0uUsq&oiEKC)vCOYO?ES`a zD%kk-)R_iYB+T@f#4jS{D&73Q*~;Om>lXBR5gvvAphiIVW9S*g9u~^PROV^PJxM_I zXa#$N1tkgAj513348+~Sx2<~}I!;>qv8ut#eAP_YEPCOzyr8LpIs$vS6hm77$`Ls- zdx#PxZ??44ISD;$W3NFS7-6@8sSP#vbZm&U$GFa%UI|~GT9LD>j2L6lm;HCzQ%C+g z>o8We=Uh@4)$W=5_;0W}3wp0volcUp{mFg&x;2++M2z+!fMqY#HMW1(qaXQ zF!~L%LFc8}teMn`M*xAA3;sB|UbAs;8k z^XYUKiT<-g2z%bo#Ye`dT`ao^Waec)SglFyxl>{+-z(1N?m9TE81yli+KO1T^+xDJ zN^Gl)X$}TD1$3xv7h}+b=Lkg5RA*dd>J#7La~$0Cpd6=4#kq7otja@4nRJAr1z`&` zT1iKL5Di6Qtk}sAmp+h+G`~*X!$s}Sc4Kz`XQdPkawf6K3sfh+E6<;Q8WjE|o^nuV z39=&_Z^;Z)Q_U9wx8H@S*F02dY<9&@rc~MQcFspx1KOx6U*!p?JXFjjF`Cm@kZ-&jjo% z`^Pjg@06U4=x98SL|1R5!A)}M!zZ>OLOjFvirv*^O$y_@CqV(F3DCSS8r) zEPBYdAwHQ`s&oGP9riIO6Y0F+!feNb%!Cvc$l(P{1&S^(o@=O_p@7xUP*J?_lgN;V zIF|AULFNu#lFPUMc>%yMhtyyj#Cx=&&p9Fzutk@+56?#HA)ol%b?IN!G&0hZG03B2 zON9CmXK3vB_8Z9BR{9wN%W+*wtt? z?9gVyh{rBknKjDo)~b0q->d&#$9MfF8YKZsqiQD z+lCpCIg}H{pRhOVrt(-m^MP|NW2x*R$6l}&>*L$&>X6uA-?G;|<3>Y@okFcC1G+&n z3Op-2G>s6`#adKGDeI4D(-Jx#hq#i^eNH(tBL+kOr^iT$N9k+Icvp%!+X9;M;^5O* zUYuORjUy@N#b$vw6g2&22bZh%O{nQ_@-uBI{_V8dNRb28Peo@E_Qhy;`uZnvQ~-{X zZkbwGWo`#ZAlYe}u9>-%wLY%kC$gE<(njlD79$q$H2T7q8CuY!B7SnX6V|;EVg&ng z_LE)EO6&NQ3QhR?X;ipT8%#bX`XcvMFy7H;`}1iD12Am{g5Tp}Lf*E8#~oXiFW>3o zRaS;68SJnbPsXZSKT8S0&`4%0PmjxmqtPnHg4 z9@xv^(8p0tEm_XxJ~;uO1YDVt^$gyy({VO8A%EC{qwBj@-O4mi^-Sd4NARZltDpN8 zv|2pv7Yj*zyCe?f|M4H0{z{gMTye2uEWXudxP}r?XWrsT+Q-Wnu@m=b9QrlDkowT>V)BU$Aollh--{5y@WWxk1;KJUbL5Vw14`LV=>MrLDf zjizPrOVTT2yIQp`r0;s*ddprycTbOu2EX&mS)&`z_#y{*rIM`MBTE!6uDS$J(9g9b zdMR_Uj;t_pt3H~|&OM=Bzlg)?hgcWgEQl23Wkr&=VZ-MNuDIZ5&XIN%RtRLS!%~VK zi|FYt>vGkCv$}OjZ@0lbiBt)(?Bb0t-BXSCV@Kj2LeICwhpy`wkb|`E^e(OzO!cB* z@P>@;^#xpxNlbZML=C8j+rrYd$E1a*$mlC@{N=>?lVdwq&OIH!&8;LXYd!7E{D^$| zl|<}ssg!u|@jmO}-TGt1x0vGyV2-~CU0K7N|N`a7$dwf%mIot~r0{J4*ei>7B-3Cef;v6u4AFfS=*pPZr$3kY(Z@y_8-Nq|K?>TA*QrIRuqv z1Qq-9C|nm3qp)`L7L95$h6Q4qmhG@ioM7)e1QJLuZ~wa}7fK+F?w%nSjl9;JiJ39pkfPc!)_ z)XQ#tt8YmqQNjidv#)`awy_2%KIqDQuGCiwjYlxQTCYWBV#7Sw zGs0)N>Q1jX=83VpKQOD(XH^ukdk>8%u!-jCUv@CL?f7;??Keua(WSwt^Q6t1a0Luk zva_k`6g1a{_L<>y+lqGUEg!yc=&^tfg0^gQ8@8?RZ-lx{j4H<^T7!66 zz6nbyImaj-+3>H0=$ua_y~*i$n!}z(*At>x>dn8AR_=X3%!lUPv@1pS6HwZZh0hS< z8T}t6I`B2o_YSS94ysMM4oH*R*0kz#{0;3_gn9S%T&us>sw39Uk@(s>4=+`aZ(5Xw{79OP_vJ2)zwW9GqwO_BFy~GGnO%29^gC%t zA(0q+o^eg4bG`tjU?+hU6aAcm?)-anM?thq=2(r~sP$Hm1WzBh)xO)Uyu6!Dvu&r4 ziE%!-=rb{NjCz~7U_v4xP1)UybDIzhH9}2-)xD76TmU!wwGs%?kJDL_O%&$CaZPL| zRC3Ox(ZAO^aus{@%A)h##=6+f^vCc_Uf@V^jnVxVq>`@@#itX6Z_zbl!O12=`7)GL z%K`0RuF&hyHO$Zlp(ghm$^wp{Un*QbUce*!%m*0Dhz#jyiDP6nvAPjsC3;SIbd`_u zTo!Yrk0>)KQaD5QrjuRk%^)=|I%-OIvvSIZ*QfMKP(RF304si1DSWvz;K$_mPhgtI zIRnVS#Wz0+`UY?oj6G-zTi5IZ#N?Xi=H2IAlAe$SwB zWmzF)8b&K>VF2y_ms5-aXXk8L;=G%Y(JPE@MIy(4I;bfCK711cYkg_CoEAFNd*QzqGI)#&*^IKJ z@l>X7debb2`V?UYW`BR1)!XIv@hd^TII6%nMo%tlMYO`uWV4eFH>Vt{ka*&mBNsd= z2HWGd9{0bv&=yXx#bQQ-DadHjVOK#X-FvR!Ewwd9YAD*`U8C#%3?Y#A?{XprVKc$Vl# zJ$5YrG{)m45D)wAFnP!e*^5Iz@l0=U)}^jC`a=A?dU ztx!4ooLKLqYkrI7EP@&Qzb*#{Gx87DLcs2pbDx{kbxNsqqJJ1#-YEZZ zCU@hjjwchvw0{oBpHHEbEV0g`P;}>9-;5q1H$W$`lx#{JdMPM}sdYDwpioa&WGPweMWKxrCP&hyK+=oNyDeGsDdVwc5ToCqU?blX;9|Q?>sSt$K41xk9yhMpm8<-? zVQNk3c{zzzQ7`UXojuIGWaK}Q>^Ry)Pyf^&ISAlgHiGqc4%5ocVs$` z4Ak%jnP^U_0lGhO3mKUQ8l+ygY_ls7X|uN9YvCd||D_KBpyb6R=RsM{Zeh=>5o0U?0`uNFk%}0@9ThF3dLkmB{tlSzx=Y9yvchx%wR|M zm2qrLzlXVqKRzjMb80i8zLwBW+`c*NA~OFw(aO~LFWtIde7i?owc+(!M>93!cX#W~ zVRn{L#sWVbW6{$jp=G@<$wcq6M_NwvqWa_4+#&G1Y}7ToFc@@HWV$-wfhok(ku(8@ zV3rMwj(kif7$T-X8I(EV$|2@|mJ#3Ga9yEUWJM|u(QH1WO*9F}07gYfNQ3)7aF1~J z)B-T{n8X%GHN}1A`nn|SPRmU)R_c@bLT1XikYcn6rjf&#VCm4 zT4XWFN5%Oo!~F2im1v09%i680S1;)4w3$tPeGN>7OqIOD64KGN@5`_~XiC5?xkB8P zzb}8OpYnG{sd8FzQWU{8RHXp|1-fT0Z?exgzN(o-=Nx171Bt>L>&){m0kS8E$ zIWpjX0{0>a2+A4gV@J#NPRl~FmF*Ydu4x2hZB=t@R-8%!4l zRS5dU53pkAktt#?GP?Zc2!vJM%_s3i)LT&{yv^Me@40*y{h#p3Xx{Ya)@hKI>+1rb ze{=O$b=KK9Hiev{ncUw<6*YdMlFPqE5%Z3d13y4xPz~9kPDu*10~@g%9$511(AQK= zsP7(rR2QE;r3w2?caKL{R6%wUfGhB0IXfeK@EV&WuR)G@x%;=KWjJC9zardl$2IyG zQ!R;)Ho4vdD5X)~-$)m0G-xsrhl<4#nlTV0>6>%3zVZ$Or$2oZbP%ZN?th<}(Ac}a zfDY#rq>HsE*1RJvh^mSxzjIv)()7hnn%2^9yyFHPWsp)inSRS#f_x%zq9JAN3@)QW zY+6;Ja4h0m@9Ru@cR!7VB1V_bQW^>Ymrrx+pg@U7Z$#Fic#lP_@LtTnR8CgdsvFz% zATifhhQeF;9HHa$Sc=x=sQln9ie0}AvRKj1_P4k&C|XlOLai)BlLiODj7@|GWgV_S zguQK)ikHy1Cn1-*F^|3Y4w41lKJ80Nw>JW|n}2>sy*`z@?7K=5!VWO4M03f_k0|jD zDLU|G9CXhB5(Bp>wOQ2*Js^k}`^X^~FARbX`859Cqluh4+>VWlSqP2|ZvZ+Bk6!qD zKQJhFOq#g7TU{_mF9DBl2Ym;+dqzxQ-kYEFl2i*qC(XGCQrUwmjsAg@T-z#SFQ{?9 zTahq&Lz(eO`s_2qj&R~HP!T#AKfRbzxR@r#sclNfEpX}$M9 z67ere1MVmGx4`ijVSE`&aU!1nStf@t$-HEPJmhX#Z~W{=Yd7v|+ZM)~g~_9GR?dFG z7xK>h*t?=oz-f@bR}}k)#*fPTQ%nH>o!fN_B94ME7adh#G zb_Y$1%IEFEB2JU$E!(br2C zv$_5W!qF$vu>5)J^4N6X;>nVR4s$`oSMMnN;k<$!%>75Lb{uO&pQ#zgCLDCR8GY4M{rb5k_Q0G#?^WY*d2D!A>{(=3 z4I``+tQn^jTTeP?QwC84wW#OxfKBhjasnUZcjI}hm87Xq)B6w(F{on1=mHbWUNX>J zMKhMOf@@2f?ro_DW8ql=D?3>nMwd1`-#$itN`@!F`EB7?8alB=iBA4KTuzxNjiX#R zpGN~RN4d?D>%E#%{rQXrjmR79!&*1WcwQIiJ2#S3B7 zLZW|y!yy>a1KfgpeO;%*jHP#)FOrt%Cdgi+Q^woFAum<&l7?$D4lxLvE9K!yc;rE;cX`79ghsPaMn=N&uWcl<) zd<9pe5B;;xFRIN+CKPN(C$~)hY&3z;+SBL-*7KaAkMY7?=mrt3f8z6U|qT3 zmQ%6OSg{DFMH{F#?`_W?+P&SRh(6Vc@$;h$pYZWfb%#b9?vO^m+;O}#E4o#ib_!PH zOTKYecba9oPkmOVhR)kIpP-{0fobv6u z@wU|q%RB%Sm63j*YBUlhbYL;06h>{UPSR%ro-n{0YECvV_+@pU-6!OSS2u%&FEwC9 zcy9Dl$dCHrq5hPgdN6`y2lK8m?XNW?N8IM{SIX*#1@lFj98(gv9yUvMT38`?JZ^%o z2qHb5v@C^%5)@BMo(MvNBG)L>kspc*wm#E*-be~yAnv+KzA(aswV(A;Js%!_2k(B2 zzP=eKXOsS7P@5c&R!c@%OGbpe{R&Izxjyrcb|OQmN5%4{ljFn$wAZzxdRh}fS>F3@ zF45Q;!M2?v*saI$BW*t%k)O|A-x$*jE(mO+OpC7ko|U47QiivemyY`m6aijn7odiM zrqv)6;p?su)D(i)?Q)3CpGbxa)S!&@#1<^VcmzeeEE#!j*WeW~5f&5?j~#AyRjTN9 zfm2g`?p@y6wbfu5Y{?P7#OdVV5>`bNyYA2k|2r9TE@C*oq-od%72FgIFcsWzk!Rm# z5XR0Pws3iXpL!_a8int-B1!OA%kjpk&wo3K1RQ-6&s$a>Sb!>o9dI{J#wNjo+~TAgwK>J>RUe3T5<_FN@w&thx}i$!5*=~NjBnj zj%G-LIN&Pn#y^X7j8=ltYnbeQ=7v9G=02VR73`uEy$y>)OJYiQ>t`x1-V8BH7(B)7 zpX})O6Ha`!a!nm_Gpz?D#|aWm5v$D4aDhoU7ngL-K`rk5u(19(Rb_*S|} z$?Rju@rwX&ZT|`zjWU2?>Zr7XnZUum>>!}p;P#4!N&yfd9e>!^vnf?XN@sBT4H*%y z_Z$4YTw0NLFfUwkZ=!Mn2xxo_QGWrCP3}(|VJtwurwEo64lYD3dKpV5;~yPKGEP+=p@ueit!28wcf5!Op)&k2htRFfZ4S7vIAS zoV-uc1|S@G9FzEcf!GEmq><$?IYeJQl?T8F61S{Uk%CWKFa7l8h)DGKeN=P9p&dZq z-(6`usP1|(ffB=1e^z7@5x>dr;IqCsh8?U|g8Q##@G29M*l>@ub%6h_4x%U@o>v~r zW{w37=Z3>(%=24-%b>Qe^72+SF=bqJzBY&9E?K9nT4*qNQkUDu*Q&}D2|sW>CZF72 zN-54C7;4hjEFR74>qK(Yk~vKxu}^+RV{BQG9+~yE zzH9&WWD&WFspu5HQ6}yNJJ|O-77%Ux*?ToFG{x%8sk(YRQeM(%aJHwcEy3;teD?lR zpCUXKMMh3{{yVuUn| zzYcjG9p(~#`?gIEsv}AdM6G;}30ri`s*#z7>4-MXtzb*6S+B#p;knFlEP^`-QEV>( zyYK9Eue)_aNUbh0s6`sk?Al033g&ZSR!NwbJ;yDJ!>Ts#7WvSV(s2{L3+lGuo-St5 zADsM=UI{IS5uUvb5UWT3cN80vy+Z}KHJMV5ukzl=M{t6VoHZzS9$bohTf!u0c4^DB zgyrc(67rbtUi7g;U-uJ^0R_$W;cD#cm);|CfH=GXKFvudOvMpf_GA~j>NjUVziA0;$wuGa!bJ9`@{-hTaVf=? zqKS4E*E$&_nJLC;2l-2PLqkuPgnuk{Q{oOQhMBQM9WL~(d%@-t@!ZH)HUYBw=a9!W z*fGq`V^qGT*G>29z=GsN98TaCEuaEGDz}`N`EAY1%Tb23M*&{ig~bf z)3|-R9bn3Zw2aL>m7?zbOmz#G6}0@4aSy|Dt29)(LRxPw&KT&ik~oC~(NG{nSUxK< zC+wunTG#Wk^A9_rIEu2)Ext!>%sf(UsJ2>-_i>f&wvw`9T?|9<`fg+drly4_$?sNY zuiwBw{Z3BDV4Qb%i2-M;M1Zq}WS@HIt0tb%onMh1fXZA@92Y{AHC7RNdV2=0U}^3zdXY$7u(wRM zC@zO6sHl2L(1xOjsh}^q@M@G?OJq75C2hpHCWFeXNV}A-lK0tn4BA7qhU=i(y&b+0 zCNqwOT=7z3yNU1;q#5H>0yzR#u4zBeUkMv9KXU<}VT9zhg%4Wk6`sVu=>69=_ABxK zJpH`Rw46m)yNODvazITt-8x8H5LyU7Lz@rV44?m|9%0c9F}Z22HZS<(+}+)$8-q$= z#OZ_+LkkewRUl~YrbppLyN2207!{)om`e1U%}xr~kYF^H)FeqpgNr6b+{5QckCFotiF%>TWu&@c?iC(n#$pb5Jj-*7L^3abKXQ zRukIfx;CM12${%oaK&r%Mt$Fsjr!iLB4s6GGZk*iIY`D`q%8!;2gcF!$T4OQA~5Bf zOR^|qXmC2UUEA2T%3M|jEVJ3Cm}V)sx4hy*7pm`xHA+m)YAvmF!;7xxbu?czN!Kl1 zrD0gRL!$u`TKEY@R=nDi5MZ`3F%X zi}0~$Sb%2hb9(q*R&#wR1y<3@5Ss*b>+e)lq8dT`9|t`Osmu)M5JP!H zfMq{~LvqCJASj?<_S(*jOzSc1iJ>#=&5TcU*b7`x z_O*zq4$L^DQfGTy92ntM31}?DFXT_UKo3WlTztN8x>`5ntp2^&Q4D>y<8ZTT4ZOl6 zK!u6g7aPOgv7wex*?4n)#Y}u+Z#fzTwY~o$R~6QNgRE2q48-q!SEN3K@)sYpp#eu%LO(mD{7!h{&L&LFt!JZew6LaGX1fTIR1(^ul z`YaOqaVHFitM7OXLhd(GDYHNOk41%+B;_gv?)wYfJu{*7l7 z{kC>r<{0pEDu*$X!U4RiIAad!cKLct-}aYr!B`v-i_F-CIUvPXlYG2nkF~%7&6!2j zUl0t+8)j08oU5Ig@-@thjLXFad#5fh;@6fT_gL-m=5z7vgx}3lrD`18{>GWa$_-vC z_a~D(ukpthO@d*Ygzsx$Yg+B`3H(Z%sF0G^nh~5&a8PZ}U5Gx`&>Ip>^lj78r^r+I z&xd9r%{_S&oY9_jBtw}SmpL9@!&pPxwSk4)>rU_rD}0Gmd096wPAQ*e8VxG+Iw{S8 zo`uW^%;jtnK6Bia?Au91)FN4?S)fe#cQ-}*gq*&?6YG3)#G5j3EV-12e)c4KGD z3H(Gu-e=t9v#{_gX0~Tp7%K9bB(aylBvmZB=I9xpFPYOYF-wzsE1AM3lbQ%Bn!E>y zttV8gULa>Hr1%p=ZNAz(vij4Q22E1p6BfH#+HFo)Dy^tsmw~&GoZ*Y({taecwqGho zhO~IIl`i@`B$O?)vitK{vxNhQ_c(R7Oio*va5Te7EdEg&63uRD4QxIedV)W7-K``e zm)SlPbLqdfZ&!~blI8r`+6C!>UjCV@qv5K@?SfYN>U}$#iKNSw(pT93;f= zCXt&4&g&uHAKId{#fKHJ)(jTBA)G?9gVaqbvC+E{Pzi5j7I6N`OPB9&IWp9o&<`~% zt>#o__H3z9V`7f$FW{^}roX=G$8V?k(Ro_tCMfi2$a~xB#C_g~guMt+?*Z&4Mq|Rh z2`2hdu{22xefpgpZm)C^bk&fRDXT5N+QF3m2?sD;}cz-@ndd3ca5hp_c z-xkp&eMW(2R&ul;vqHj#_?KSX6z1nOYq)PtBwe>F%n?N^w=7_zTQ`#Kd1vyZTH~9a z=d*tI>d3|*-qCAoBao6E?x67A=bHTd(;?x5O#SYA1vx7MnHy|Q8_R}+6g=Hww{uNw z34dPxG>E%In_s4my!KPP_9LFJHdx4B5cjm+IJK7KU-~%wSpJ127Tq@T8@DS79HHld zH7_z`A@!l63S!wovOE3+oND8EZUI_^_!=QI)0jyJ5HsteS0}5ibuucYgPk=RcJ{dx)t=_Sei&OL~{$Dsl0>Sc`-?_ zEjlCtqu5z{o9ch{C7Cbe4}-AEtaT+ILfKWq-81MTykPyWz_& zE~6hI-xrN}b(o9uG?oyM$z#`MPe{M}`?vg8!bDW*c(GhBYZFP}qY8}uB z)fuBGtSdA*LVJ-=nPE>dl2n=kShzkOHo^=2&swV~!I6aL=jA+krRO}coVNB*l<`}X z9F_>dQE;#8ubCk2^ISYJUmsi96f=MR1W&G}mESLw_pFyM4W|==Ku%FX&99ByF0ux& z8hR>Z2Ayy}rO5+#lzs4)uM;XCJfN%kdhbpYrUN7%uDX~f}W2#>dwyy#zJFcJ*_LYXQs z)iSXvx}O%5=sb$8VL$v&b{tGw%Ot_9=j?=bGNk+;3t%j@k|Sx=OKio`H!a^%Zmc_& zn(h*aN?FwJRTl#FW-@h94swF@WumI%Dfl%2)X+vKV=|i_WVphEGNr^jwd{?(aWHnS zL3^)72ha=xua)XPNYC3riG+)ece^JF5>t&ZRYfp`EpTTQ)OM-lug4!xd#(^mJzu*+%8N?!nvBF(WV3CAE zhsSR>A%2w;Ujc!*h~{P>7dUB;UjPhIFhCJQG6+5H0u;b5Bg7gpg*UniQ%OTJNEHRw zBCvSFN@>2yaah;#oA3a{7+hfiNlGE$BYc&BV;UakSAo`-a0mY;GBL2#PuClc>cL#3 zrY>k&{bxRS9uNXBRUI(*0ohCB1J~4d7_d`rc}~oV2NRC@kp$U#nn3WVK)uiZm{WA+ zG^u$E5Xb$_-FCajg!CGXfbLL9sPIR^tP%W}xs?#2&SK2#P(Jf-wngwAU)aoz^i81a zj{=?Vb@te9v#iQ4urTd;mkYsZGrYEI3XcA)8)o+ZIdX3-^;_*t*#unZK$@weqn}IU)3vA^`o~rOP9E z+EguiewbEw3kyB;3mR05z=xSee|^=-M&&qN4k|-oTEao>Ejp25bY8ZsEbF_UeB+90 zve&=8`y3f?2z5fwRJHuO=!BI4oYuZllF`JaX^T(y5C?w}o1Yaop+Kwt(6KQSbMT1Z zY1#|6Zt3)7jMD%aYkr)C>bIjnF`QSC7%WZ;3p5=s1Linci9W+(No#Gv=RrZnev10W z`sZ!wNb?poZrzoX(Du|eu>76vPz>G09>@gC>4`mZYqawN%S}!4bvg7^9bGuFy?@yU zznKn`+H&{16B4^eu+vR_X4PXJZIO0S2l$7aB-Ph|L2oHW1Np_yr%2l2};E>CSy=RpXz)U=T0Q2)D`W zNc2CohAdov@Q<=W3zrX*oY-VB16H?TuH;1{Q7AQCA#anYF&9%y@nJR587GP8tm!U)Jxjt|L z`E;%C*VX8mSy?y?`+qxI$c%21NhNn!-UR?&XG$HGwBu%zP8_FCQyG={z&zs3_xxyZ z1UIfr1FAdgK#HM=`IO#9dG zDK}8E1xt@{eL-1)>oa;oeGckfqvk5iw^vShD&zvs{6}VXI5-J@iD-)j1{yupq6gx1 zYX794ba||agLsDJgu)ivf$V;yFYTr9Y<^#_-IiP$@q;KnaRCdc44~@`!EV`GzXV@o zKPGySsZ0W{5LB@01I$NI8lrwNy0f(~xM0}oJzz@9lkcQuTHepy?-1aAuj(>MFRKh?ER9WJ~AGC&L!qEeZBnYXhD>;{e!uge-7GcLjU>i>Zfy zG;JK%idpnY`1)CPo@ia$pPA>iKe2i%3C4oWzcttzxXSpP$t!H?d`Vpx3*INO$R zD?e(!$3S6NLiM;F1keDJaQkpX0}Bu?KS9YcMRyth4$YgcGG%pdyGnG+x_7PeY@9P- z^}NG4J*Qa8s><}2l8h?#ghF+?inbHQn!4@u=rM0LIZP)tIW{5u{)x0fJLXNG(b*fJ z?P%u%X1wc}O5)_N;gU*X;DGYd>MMX39N@i-MKi9rfgh;uiuzB|_5N4I&B^`87jXL8 z5ljdKMSlcsm)QaLe&1By;4_<uJvY@v#yP8*h8#Fo1nw3zbl?)- z@^2YD8EJUKN)SWL$tF~_kUjG?ccxSwMGRt|3QStpvj>@S_F*Ds>Rk4C2*MXrrcWyK zcb{=OE(pNqUa&`iEyR%#w-jvee1swlf0GXgD;}%I;GsXILZpAXCxm$cxFAmb5n_0z zeByEcDu*S5fEc;!7dAQ$#GL!7LWU>S+u)c-dx4p>EqX1e1|3;UjNIx(>i4lL04sTu zSN9|cwjVGM7`-M&YsOezyV6F&gTCktKHM`ixd>m9cJ(dX-O+YND6f2YS~<`kPi4ZA z-Or1sF&g~A#0mF=n(}Z9#0s6c_I9BZNmz4KUkw^M#7~&Af~Nu)pom;G|6{-p5so-= zg8zoeH(T0%1f)XB?Wo6*!GRmeK&_=|FY@v4xODGk|KBzvURb6yFdqSV$G?%=6gqwW zZi9~C;6)8TT>e8L3h?Q$zg&^*6f|1QtVri510YGy3y6#9^W+JUaCJ*WsC7(`#N|9| zHGS@!BQuwcHszowoM?Rfl(gGWcz-}54PIbCx#SP?j&ufpe@ij7fV%O^T|`5*$&W$T z|4vq003VOO8QGFn&8P(&UGcY5TvVm|$=8jkDm;2!ypah5CO`?(f(uPP(|E@fSnp64 ztqA?|`DYQ*8|3xx#8LFaaE>{)E#b-97DVq)!_3MJoz%zlr^vfF+%12_L3yw5X(hau zkeOr+)_mb%+KiT5sy|SI$LLuaf_Q#m=`m<{#z&!@Xxd5a{d}MVvJ5`84ywOb4S9LT zWJjoZ%zfOolAzTSEgDsvt1q3TU&4eRNME!gq)^zPRoLKm$@KerT+Vv(yZ(Z57SuCl zYVX&X;ER*)FLlf|CzyBG@5m{Dy1XpIgf;qCYtaA7g-5Fko09Z>?AEWYY;&*e2X$B2 zhp-T0&>`J>Xn@5=0Om&&?OifAFv?#}BN?FVR@@Fz!~@f|qTxhA zMxP|FS_e1ny8dL*L#gJ6y|Arc3HF72wvBM-gH5);O&pKdsvtgn_Q1BU5&J(>iMOK_ zfT(5VqKo7th>kv3WHuy=+asrxTp%i3fr`sP4Own#Ma)*JX3|ArM8TZ5T-$KK!RzW8 zA_=5eZO~jcS00UYH}b{GC=<)ThLM;g-bpdhC$yFxFH)2z*p)DVYATa+FZa9@q6-3w zEw-_`y<$=L`Tet?XX|f`|NMyo6L?{c%1lkJ+ni$^G&mUk|0UPi7k|6?+sW!2$RFSL z95a3%cXuNH+?FOgWM&U?WQis&;JvjCtlb4Qy0VygAOV&eL$q|Zeb7Nw>-pHxfLk4P zGI(pM%1uI)J z_5I*J09e3+{MPZgOE0O4NuCPc_kq&Z;8P+QOtwyqdTn4UqB_5-s9T3k{}oh7I=0l8}L*7CE=3pB};NSt}f%PX1XT z>K;foW3vykFFrVK2i9M14LRuxxtjF%C}?f?4Ob%1Y@-&JR~7BKGVdj<+J^UkJK1MB zCoefA%AfY7FpkDim8T{aX`vP;5Z$8!JF?w#FZdxwYaq#Fpf(?7T_k`4#BeU1H}4w{ zZ>gz4df$dWtP#NWTy!Gw-u2w&skbHD1Y}38d3OnD@iY>?=<9t`$pBdGqt}5X=4{%5 z0hAa2CbPI`g!B_-EBJIiS56bWSt;*WYbeJAqDr7aTlc~5j6gj6Q8{MVj?#|#poBWU zn{9sbUi!@L=})m`CpF6kGJS;&d%#3$v3IK_V#TS3NDT{<+%{iRO}e}*pGa>E9z7xC zuZwgFuN*u-pU-D+a!00=qoGQT1eF;+H!N=oU+rYb-r^z$RHb(d#dLQXK}xU7M;x1R3$jcjMx@h2x1uE1#;Q-H7rq!5%>yzVfIX$;mqk%A z!80tKFRAEloUD}bY5Ci=7w^Po$tYb<7U^0Sz(f&D3D5Ug%;M zoAyNxs#Q{0^T*`;1^y}F`LZ6;c0@Ne5lZ3~6Uv$&*kiGuLZSvV=NcR`Q_}-z&DgH#(&pGn zm*j~Ymws)#3Bze*t7&vQmDt+$Ox-cySKF`+L3jBN4sU*OJ7~2)l4|=Z`tpu;(C$BN zUpOlz{(Pf!*A6;9yT}->GOZw`f)?;R9=$}#DT1xsHLlfGO;#OZ6K2TXFyi@3U+5}# zke=|H?R!Xq?DI@HTgerhrCi#58tps)R0$)%#yTHbMwL!6Fi*vm^uV>` z*+dk;HKvzPOtkfjks-Bct5{I(j&k?7m0HO10}mw0qCKD}N5r!XE)M(kkD=E^H3g3# zioZ!`faH#v@^r4o11Uwy0DksY3JhQh2EvS)noiy#m_z||pIru$Rs* zes0t&4k9U|UG_g}+*_r>5cP$RZ^ltDUm9fw2=vy|^9SLA5nzyEgL~+R2lxX*N;8F- zo_ACQ$LVbNGE&{#!aR|sydr$}a{xd4fe3x!?aB2H%wMcoXff4g62{Ec8(kTH3)Rt$ z!*B0P`pJ}1UHfQu)Bf#mc9c(=zMS1IeLW^MG1J>2{QY{a5gIA}yKkJZ+)s1a>$*m`a8N=E{DHK2)T@vCJS$e+~|I&jblTcd(#h0vJrgN@wskCH#o zPVK_gMqRWwITH|1C+&zi4kX5Mx#8&fN&Y@Lg}W3WA7kARIy3r>aC&nVpA>f@eO2z-=Vhaq$7PGT1iI zCSh4ew4Wa$Hay~)ZRpVTFkh;aE>SIIkpCctb+BXaE2aohx6^PJuzc)wO$`)QKxbQi zV=r@WClo`cJ*nVUpaa)FvXpE>j5nL#i(Qo%vNsDsSHy3d;X0j!@!B`VhM|=^EGu}Sf}#J_XI>} z>KSNGXTtVhIPJR=<-1)Di#Q63aI74B={>7RvOWkEnv8iJC)B7K=H0G?X>vebsIdrwawH`ta{lDRCEH|S5GUIgX+wW25*&9h3E1( zrhDQQ^|e=m6+VZ5h0Z6e-n#$b&LpoMcDj9kE8bjkX2}q`<4({&amZ7C6Uf5d?0BQ0 zU%B{t`CxJW1Yi~FIK(c}rSg3oK|4Aj{EC&Y|0y_4;fZE$@BjhEh8cq~tVD$6Vb+Rd ze>cP4{m2@MID-g;aa=GD5Won7H!I7dfV88P>YD72-a>PM98T*x{7>!5=9M9QZzSG0 zEv8^dQ|bPvl_v!@oPM(5`!W}V`SV4)X*GD-W-{;W{OgF9U>z!fQM@G)kJ4nxT=paS zt$|RqHfzoGmziZztg&lEW zs}01cShKI5$GG^j_B2Xt^(|QyNN;6M$fF5m4d~O6FS{=#zjKn9<8Kl|cme0;s02TL zK`>>LcJ|2~a0j{`wJ&QR+1MG#uAW4A;^1}YL-^Etk&BskbUi`;&kNu+TD#qzBZJd= z7-+NsHT@fVI0QEfC!7F3{S1S$@PM~$F3&Biq9e@k^5Grc`Dt(uh6qoa^YXv~##w0nrU<>cX@AubYtmiuQ`lg}46-^+xK zduHuNql^d;;Pm7xNIXZ{Gmv~*e@YTN?Crs)E}^k$qLSr7#^r(Ev~dHZf%7i40A(o5 zthZ;B&A=_Y>Igi^mH}nT$+t^uzUSo^zZY8*o!-IKHr`=MZg@w03v?iBjk_Y8%r{?Y z`%;M;MXJl!wMVYW*9Dm{_r!7$7G)vP;v zF`szE^knX7=iATAq*$^%>Br)}uM0B)4=7vDxq^X}T~C*xcV}Etg)+p!Rfb=uc?jUU`R7t+-^ipRkMQiq{z}M2T0i~+;t$-cOb@<*-^W}%9gedAB(b+Ixj^9xk4JivN?A(* zQ;j1ZHsQHG$`v8mSqS+1PPM;~{Y4`YA6*x>b8}HyIPXN(+Gco6nJ)P?K7>hCm zqNXKYYbk~H&|x1J=#QgCv$5So9(rKIvL}Zg?^5V|xI*xjkpAym`hsaPp~pjuyi$8Q zkA^Q4c9E+}{KwYxH8y$->o=QhtA3UbAU9R=&-h_&?;`g6g(sJtca<^bvG?GGuw8HV z_A5GJ@6`b$vlj1uJ^pTQS8M9v;37Q2xsuzR2r?tHfZ*R3Qdc?5oU9IRuB<5|QvLSE zq{!wXu2j9x^z^{PP+|#MrtlPNehGzY4f@mUV2oCnIG*xEIdUIDs!3z`1@GR=){wkK zwNXJJl}{qUFs{)0ibMCTvxidU6yCob8NG0XO~LRZJ_&UIW{|Ibf{X2jFI$4S9zZ0i zL_u&_EwJ-trJkQGJ(U?@p(H35^?;t#zFdKiq(mjf0M3d{g6t1WsWYoi3|virO!S3B zG4>805f0|~Is>*g?Bnz2g*(g^YmeSj?SPy9yKcJd4N*Z>nIi$yGqPVdG+M+^y$ABo zh6kj4NE#CX``o7Fzv#?n4EN*g5(MCSGDl(=_jWmUyCqAu><{HU6`OoMYW*5o$@C&1 zHdA<9mv|fgdNwTasAuq~XY$VJ@XV$0HmvZRx;oxE?E3lSnEr|)5DWMqZ1KG3An`n0 zJG2Dv(B+Mt^#vK|IepZ=wOkAtKL6d*KQsl;;N^TI6TjhF2#`dy*tw_fa$adn zf}d`!oxEp9Tv&=E4?QrnkQ{a)RjhjI!G7Nu~G$ z(&nA162P&4Bkb7IpFf$Ye2aR_dj!;ZkVNWpeUJ_|5A$GHIgl6Ac9GcHTsn zqy|s)lIQ&lff_Rl9Gw%0{T5iw^s`@$gEeBLY3G-3n$(dmg&1^Pg{!}vOzK25j@IzT z$&6q(zKyahVmcApx&kN~j*QO(qCGOs|GNsB*@st2q|;$W*2DPU3LF97i~Lo0UQQwB zXsOU1%{FpW4xQLs305)_s8TZ4Z%ajVxD;0;S$t0dj9iekO1=d1IdTHN8|0_!>tW9E zNJ(@?Gbg>>Q*_U;Qb1(Me7XT15Mug0;?hXHL+^JQp%gzT@yQK;EN4fP`9TuGbBT+M z0N?&6!^a^~u{1>H*WfjQS?fckv7F^;kH;k9s5|-S39VA15OxYNoX>4S7g+Ern-o3Q zumfx*i9*lB)MtMp4#DdjCL1ved`R!`KEXim^ppM3y-UHoJurKS4s8yW9FusEFIj)` zzcjIW)U^ftTQsU-buh2c#axoC%=lLnICvx0yh$AnBnm{W2m(!^V$&%B0RnMpKd( zG(@-CjZN2MGqooITZ*|X4mx^qcdxJ}{*$FC{skv0u*ll=%Y`N3ccua?@k-i=l@9Ky zDJP->{vFS~T{1)84kU*hlD;KZrXozcODRI9Gp@@KJ!m{q!XSrBEHLoXop0Lfwpd%Z2c@lfDL zhgZ~O$ejc0?HB8{c?PWxMj8w(_R%On1~L;HU!^{TKTBalWhDsy))WBb)j^K zen_NGLp;Af!!d8RZ)KzGSP%_cAQ1ceTN(~wgR9{p7`hWAFlTFR2PwK$6Assb;u48} zF_}4sL6+IzW|J+I)YcLZgSNZiIk{SB1_nZ)+nNh%?fra=M`iJ$B-9t@@+;bjW`%^s zdaf!K#bYATE|$Ym2CUmJwxp2n*F#1Pg0GJ&OYC`&mFJZ#`N(L^_>_{UaE~+ZH?8Ag z_OG*9b#NU@ldqf<;&&VLeNSm%yKVvw+Skrmgj!8P_m;}fHbFNhS{kVcUqWHx+n*;# znNPgwp!zxWDjDwi*yeG=|GKiEm?`OXMQLksRtW91X-`uy{jTcQMIqOaRr$^oUJ~Sl zCs%@Wed~277omxna8a#_-GJG?%N+Qo?^DL7B%-#RcIp?a`OfYvy@&;#;y|r_!s6Am zQs?mT!taker>|`P^rtlVc#ob9aAMY@(?QbZubAk3ke(_@F?0djlVsL)G!G_g(TylN zZZJh_p@1(tT4vs?vkMfPH*%a|Xq8P(>+xqk3M4e-0WSp_&y047y15h#6slzWo?)jU z{3cS0gDgDg<}-e|*cjYw+Pi@GlAQ#-Q28+joZLQWCN5%11XT1NdK$7qsn4Wd`nJv1 zOV*3X0pS?IxK!3*88Od?675KMPr<&z;mq$0)=eFRGF7N;Eo1sWGA8;Y=gem-5x%zO?KTwsijl@GawhK14(s*J-E%Mvu_7NqtEuhZ6( z-p$7!f_W*Fj#u~+FD$zaw!STx`x&U($4wTEtGn_a5FbxJB!{m~P=H@_K{U?5adzEx zH)60J$`;Z~={SF`&sk|bGsR0E3F+}Y4dLrC_YOZLO$;r}-@&AY2eO_a zg;K7ghB^`X#zUR++Lk6ipMfA~3omQ}kMrREE&bbztgSOx9k}~8b@j{z8u1PC!|!%fege>VMs+YVzgMS{@0w zhS0V~Z5yVt%zzP~{X0jw53%XDp(mp0#gi5aH=id%W44=q%pA26b9)YeX_z(Xs8Cw0oNr%ot|M#3> zY0ZC`c6$($!d?>4$B7EXb}+*3#bf}JFva643D8evm>ToRf3)vWZDcBR(}pR$@R!-k zGkU7f- zs0KChj_e8)Pco%P@IWhy|3+BaQJ=P+muuDiVIHVA`UQWpcaAZ$)?1x7tQO26`A?d1 z@Y~iKJ1vm2Izvu(vkJJqXl5I?rj!4=>D9{pIS=@W=0U!3vH%6fg^WBgo|Z)S*G+dD zK^XljFZ6GQ_F*TH>ey2wP|iNe3R97@oR)HU?ZePLy3#0Wi+`woG+dQdmWgak(Q(+> zb;K3<8WME{&1SKEIRgbWqjDdEeSa+|0wrB8#S|1`ev!EL4w0gK|~N2Vs951!N@+yxL^K z?knEfA%jgw|DoU}sK9`rQ+y#M<2NLB11Bf|$SZ>sbhP2w#E2N>r^=42@IteXHEAlJx zH9=+rsq!K!TbuINu98Q`q5UL@6{xhf%xHHcD%kj3&2TuG!J4RZO?Kid?}ar!BRwn_ zfGh@#%l0_b%rKu3th5Clr$pOXrepOOyof7xI+6P@`K`Gm?$I^bt-zG`^Ho#K**6z9HYQ?N&C1B7P>LerLHfvZOwfxmnLKJ4_gK~PmD#+4K+lAugO?R+28&cI3Wptq2 z6HTK{xW5rSuMO-7cwmbWPW9S@?3r$NZG=9e&24u=Oz}hO-LA;?>N<(HovJ3pZmt~w z-oIy)HljgtUVJyk@a(fAr(%-TUWIx;QpRYpd>{j$PB0J|KJn8UJ_VTu$%p_z;lk`I z8HeG|XmxyXcY*K#(tV-2*AxzXYumIafb7L5M_r;Uqp-tOM3;Glhza7&zM1Dnd?s$Sg*n+3jvTe8M|3p_w&%ch2%M)4t8nM0$~)FKAIZ|F#|e>6HM zDcO?QNWbt2`@abrInDQ)v24u!NiL$=B<1U}MRSxgx0NGLZW!O9wl=SzrpX@|v~mV} zIeVhHF2T5*(JhuewR}Wy%8htOZPupFAJlKsn?7s7%C;UKjmxsL-7haQ!hU>mQfkpt zKiLT%Zfa#bWZ#%q`O9CHEohN4P(9j=LR3%7&Wdg8TF=3#xRgLv=!T;ge?hm zZSegqp?Fd(#~0h$HzrOn($S(!0JZd$&Qon~pEF<4M@Vr^On}u){gqvOJqGXSl^m?z z;4ZMI{7hm=lZFfii?>lA2}i7;Eu<1#18wt;XDQHr8$XEcQxSVG#`9Tu`$c=rSHWMD=E(jegw=oggfyD%^+TMU_wc56vFr zOTZyRoA$_+!%Ix|e3z;D-JATAAejoW#2TaLY65Dh@DDA|0)i4p@2sy`eQs3ypAaap z=xT%oQ<&NnAGP3v#EkbaPmXDF{2Z31C=gFu%+4}ST_94aQz-b@%&(p%i6U2qXv7H( z$$S@~ShwG_q_O%ne@YIjt>&FVlz>8I^-T5hoF4y0AJLkLfS!t2Y5N!L=B4kgmvNmQ)6%hA;5|p4*Xoq@()aP;-Y6u5MRk-km({9{l=o zjrPAxVnk&bM3a~06eGIs#vQg#kLn+9ErdhSd=JI(a<#RW$!Pbm7kxKsJ2o+zf$>Ut z)i<0C=Sy)v$&4hTH>I*c!#j;j*qptu$$Blf9kYj>>qr`^^($}4v8hP-dt;-lPva`o zC^K<9O>d%fg&k+jzsW4=ZFP}bc67uR5rx%LYmGzvUe{J;!+74QZa$SqpKH7y6# zvru=09Ftm5wnJ{|45ijlEvl9j`a2m0R7At(_Tn~o_BNE!khn7TFG~xfY#(HPP-4?_^Q{Gpk{p{0EHmDt*%uM@yu39^o12ktkfNAxdPbcCmEJGaZCe z-UJrR(?#xs8YuoAs9#OU;Y+U~#(7~8am1RO+(uiXe3g)&CBccH3~E{MAy2bqgcg8v zRqQtdvnGF?L9pg{QsyO2LOkB~a^w&Vh6=7-04?ypWfR8C`J%EV%R$NNTuXLeyksOs zGmSU~tUv+teq?_$QBo-cBY%D%Vtr6s4u2x((`S%S?GE3#fw+U#R<6)QVm4?EtPZ%d zp<}q&^a!{JAghXs7_@+n?~^9Sbhj(-ohX7Id#2KT5Qf;x;d6Z8$bakB^c>NTSX&X1 zmw!k%L)CrXi!lNGhV^>zs?d-#{~K52YFu z--B^%MV-hFS^XKjVGhp}FI?-%35nP@#*ZB{h_Px*sBeff}p(UMUD^K#;B7 zi2^d{5j872M)h-}tz!fq>Th)-*V3AZ=cKT%=$4M!UFTFQpOowOMs)5_-0*;m!rMO@h%j}B=@I0uBd4qz6RKRsolxMNM&WJpU zV&~-R3q5m|aQMeFLvBx`M<=xDPS~)sKrO4D&R4Nj6ts@GXS=~3BpOd^fs)eFuwk=; zfb!DPzE8>_w#L7;pR4>5#eXV@)~ODXz8lXcjoP}NQF(q2_x~6DeiD-34 zf;9`UEafRbUgW^*O1sB2m^bVMkYW*yW3nshNKf2>qz}`&c-}M@#hSr`44}0@|t}a|F+k&2FZ{kx=SfwuTf|tKNI+$fL zQ%mkUv23!QiwBpi?cwZhAkDzN`!o;7=@SwQsavnog$X;kuXNR z892nuJn=9gSp2#f{YPn{@eOsaW&lK7nlU&OV&le)fjuBZJdo|bXNC;j&medrITXUd zTt2TOx~?zLRGob55Kj1-Tt6tb+q!Z4_4keEw#`>@c^6Foi-!QXEfp+NOtrMEYl@tR z*^X<)7dAHD&=o)E!&WaFbLGecS=fNlbYs%i4+!QJ-TI?1d-Qyi^}~m%3u+$I`6OcDxc2zeiO-*F0YSY2}vl;uT2iZ>rQ z=_1FT{T>i$d}eAj}W>{5n#=#2$My=7pdWw?a}w(TYyg zPsW|E?@yyG-cRiQZ`}UGaA+H$wMTT`hRg){|MLQb6**pyX=JrILQ|@-+46gm-V^f& z<+eK__5_luHGdVqO9|J~|LF8sjaEyc8%shU!sk5Z`xA-hu6^1)40$W$W4HdAU!?^k zrt54nNdkxStBR@%_tg@JeD>cuE^NB$POrbB6F+9ztEYxY!cl;rl@;zbwqg<^?@}1pP)IGI;Yo@xq4V zR5tlB;RsJNj30VRHT88;|0VdSz0CgeoxBH_14wbCv9sp+5w}(BB|NjPqUVTOfC&JS zms5ntXXQV$33^2tk`N=*#F~(fQGt3h#{02l!R*O#&a#CU8X2zi$fxdQmUY&y?_#`Y z3|nWy2tC_kmKB(HZ6u*&wWq)MYg>*tNm~Qc0s>(@c@2l`VKO7MjetRk@?i8x?w*qp zq^~ynR*a4o-=our0y6uwqza?$UdIyqns1m3W0mzfz9{S#%#s z)v5e(#c6+ehsm7YHvUfRg$x?6-*gD5(zy(?lw2~45!a*i_til9W6Qq9Oy^mgx8r9` zBwSpr|Lscr4erGKls6KPp`eFKP5U(zOhFqU1YNy^Ppj6(*}GE%(~G}N95#{`7My`AuvFXKOFZw_4KZ3^DkE5CLXbIXZ`&fH zNajY`H`FRg@F6~$S71lLmlqFF;N=>xDwZS@mFrnh=s*PMQI4A}%;TATXcRKZIeRO{ z#?ErgX>R58`mkqu#a=!YIo^%TV250V#GIcPh&U5e@{B zVXz4+vbl^m{Cip)ELOG$h{miU!raA6uWd@={paP!!x`%U}(??&JSKg#(WLHiQCcBa*F|k z=b4rZK3w}~z+n)Cx4>H8XUz>=1Qxg?xJ^Y`GP#RNSL%f8>92hGa3y2raYnC{7H%$D zp?8-7|8oD4QH(w`90-tPrGfxu)>4Th#|C2MsTi@KfCq|3r6Ac0|LD{5>GPF_7p+FMos z8tWnGiJKE}cbnm01I|YMnIA2#@z{9vNoQn8!z29DeOHdx?neM+0h+ysuOluX@jir* zD)t3_B@M~Ow5%a=jVUSAWY2Fs>)N^7f}AU$9i-f3wYQUOK4GO{th`LON!+zdXyDPW z!*!dsKZS1(E%SWWZGtZgiLfKOw?z4VJwSc%a(~bnof$3+AG>pvUV@sL)b~A7`j4&p zyv{D5jE6X2STe|rsfvd z`B*p4=d!<~5*y3ay*@8g`t$rf(fcM#XPiNvhKg(c(3OK-Dj;bs?V1Jg|8z_qC~;gM zepgsVWjvNd`raLUd`?M%8}4}bF5WY9c8~TX3Ox?h4x44^7Z-zBGTLo*fi!|saj<8u ztqhhZ_o@4-(-~VyHGEk$_HtKoC~`CGb_0!`XaG|{3PYgP6pA@iLLVNn1p~)XkTH*z zCXZ#nly$(AW5SfDr%;$?(^{tccVNV#FNM7n%&~_^A6{}oZrm>_-~nly*l4cc9h|Y- z;(>AHFC?o%@xSc-y*n}?wAw3C{A88NVJvU_)*cy`f zPzF{S5nQZ?2|fmv+k6~~Ve|d(f?b#tBZ<~f?t)Axg9w|R=zJW9kq9M`!|6B@pke5B zT{uJ6s?jG(*g*|!k{Bh=e7cj3u#)^%B3T{jZ*KErKG1%CX@^~qvUPcW2gV$Yq@dz5 zl8!}wRUM7=`DZ&W28>2`*ZBCtAN8UOxA>&++r~8(E2b@6>(J#{20v7~Own>29$9$w zo`hm_R?w41@-Jx)iQs;u0*n zrDZ`aW>Q18!5P+S+UR$^)=itFyQl5_DJGSF(JG~=AfAN!rO13X3f2J)U&3+8`I@@* z-xA2N5-#v4DI-|(PUtHWG%-Y%MpwAbA}Re{%sq%lbT60!J3iT^#SH%Ag#v-_CgVKb zff{{zB_brQU$Zdk5%n?I@;*j*!TSS{rt^dgzZFS`JD!YXPJL$dQ9c&Q_Hti8uDs~J z?Y26!W{(yeC;sxuk>SsdT~}agJB2^{r|r;B=vp8s8cZVf*urnoRF}#Ch{Vs{b2D5$ z9=7w$5C}hdhpo^*_^kglhbgK{Xk)j=MV_Z_mTkZ>;Tz*`;{O?&O)=kIh_2CU>OD5( zOHQawSf26v@nfPG+?~zgK$NM7$A((S2|jD;7bog1rek6ggX)tur`{tFO0HtAwxNgX zNQUfR_8Nbp)qSwfIdXFv8L%AN&^v8Rchu-CZuI^tDuP|CHq3cg-#k01(N^Sf{Xl5A zSdHJW)HABnVcQ#LsySnJG1>5m%d1xm(b+V@uvj?`*>PHR+w@?I9K+DZQu-5pCme5) z(HST!uO>CR>P*}-JN|(}(CqIo_QmAtWU%7XBVum1Fm=1v%0d~`vwVd^;O4&g>*)mdo{f#$X6!@7^j;{n|9)h|UgKCHW z1I(ch3-UEQWk@u5+;|7%W>-^+5}+Ix+AZPEuaFCGyoeEaK0lcRzxG6GN**Oq-8Vs!W&f1gTByyT#?FQeK}wHQUFbYpd9j^&E5 zrus&}JOSZFU`Bc7mdGlpR0^RbI3WNG64G!~laQAw1kZ1#W^lAJCDs8D?J@$nhU?!g z9O_=asoVJg3eq(SE8zZhSM-R;?RTtIV}a2C%2)S!0CYAk@LF-NU2sFjoO+3$2#Enr zhL!o4RDwO}>%GsutOi`YKV)bD9zo zZIlb!kKiSu%ctBc>B!LBzbQ(Sfv;4&tgp@;{8?|=V>R+*W|G&L7R%Q5GEEI8un2yI0r;MH9J5Y4%v5lmcVX{5)CbgQJWYX|7_BJQzVVrg|%Nt7k z-~-9<_?4W9i}sY}Gbe9c>w4SiHOrzuMsL&qqv;zMEQyk4+qP}nwrxz?_Oxx=w#{kV zJ=3;r+jw`s-S-RX-mE$&BO)U+?YQx2_rgBva?<5Vf8jN---Ms^7dzhg6Y6#;^fz8{ z>3VO6{{0XPZZ&wLX#JVNY}_$6Xt>l{9z3Vb$AGveM^bW$)j*;GHb&S6mq$=01ew!D z{67!pM(3-43QY-qh2?J|>&s>)RDQ-{w;Pj1gRn+{hN%BauVBhMc48Fv82U^K zGd@bZnMNB0M@S*5kiuf^32E)Y0QlZ16RiApXfD=2QqWFp;zNSrVMefn%}iWF*9Uqg zlv77dhtvjR3E^>I1VwRCHUmU>_0Ej0Aa%w`>TY@FGZw7ovOZb*a9uVO;pM=(XO)`Z zol7&up=W-S3pgI`Tmy38z;n(>MYVHK#@5W^fz=7Yxna;YVZDHdCPKy;?@Y(BZ%IhX z@^1nP;xEq*wZp@u4tw}F-&mI#;2Ul1CM$JUt6lz-482eLmrdU82jgEi5+||9A)iK6 z_k+W!9dN!UoS42l!xOhLKj=Q1{_q3e{@TCa{v9Encx5zi%N{|2( z8@KNMA_+khD>n*@6U6sGHsQ>h^88q1X_FzkQ3sVRoKQkV1=KBsV;@B*PqLF^(nQU| ziGSht-`T?b(WaZ$=Av1bmPYyX^E0`gY<#43Gwme=DUeAs3f_ttAn>|Ke~`~~ z98rNK!?Q23H~fm3^q4QR{K@O_xCkX2;N;+D{R!-f2X3X=>}HN=o5?e9(dljL1N{j- z>S+cW-+e3{`}3c_RK~HK{m^9B_qtSb{F5Dm*;g!i$1M_vd)O zAC8BOTWG_LI`H4@5zrTBX3HZ9#AuL-?0MJ7IOL(1$`O^1FXh>cN*EdF>-!F%moVlj z^uJZVRmV?Ii-KwyVTgk!>4FtB*QJeWYkh%ex=LWl&3_KzUh$9jv7ONX|}rs(@x86ng3%xDwfu zT=uTBM%F!QLBMxAp^dQ9U#Y^CQv-pj?~u0VF2bWz0RM3+)E_<6SpkUp7Zc=x^Snu2&p(!NZ#ghcncgd`hk>{bA_y?uNn!M zy&LdB_=cqKajh;!(i|vDk3F&)|1(qdqB|}6QQosqf7p^!>P8EeZK1wFvG3veu!VKy z!`w!|sLK^uxEf%Ji5L*m$@uUJdQx3~%=|GLCrYMPtJUGjJL3;%24!>Gh#7 zxa5LmuBiunIdvT5Yl02n8jXdPG9U-JikP-c4B7qdYrV7mbhvzf;2ky8=)1(S>6;-n z=Evp*e$^`$d57FNaOp=5hEPaC23=&#O2{d}jccQQAvjlgfm63W7#3o*F)9ZYk-qcucSpAg>Eu$di^JHbbpv7`Q z*Pe$3)a9%zU^{Wb5rB(&&{f`$ZzzTd^r)2lv|La0Hy_1U^yOKX)@2P#NM`G%CJD|trY%cL5GUS2hBWr zE@hovJo?xu+fzveFTM*E0S15|S)_#Ycb}N?t$QAM7jp)CvAFzfFGKktd`7!+~ zKg(s*xs?2Fuy6XBHM8D&Pt&ef!1_>tdpX{b&%>GAE}5j$9keCA6N20aH|hsvaTUsi zzPWKl+c1gja%(Ow-ftI-<{y7T$tE1XwAtYG)m6jBarRZxU9zQHzj6n8@zdo_7a<0J zRKDMjH|)3DX`Q#z>8tncV_P2xH&-%C-A(_oZ$Gkm}MM z($_Y|43`XNmDOoU8Aa$j@@Zvc1ei<%>iwgZB{XAhc=~2S#7(#}rN@L8(3rAUNj;?s zGP~zRX%e4=%7pHpuPr3GYS}@P!?4@egLuK|Y5f04Cmcbhi_vqiKT&|y=3tB%=A+V@ zzXGxf3mdoe!V=t3e)FX*aY&I1j}>+iNBnF1YxT!!)qds^cxbFbfUg&Ms(Da?iA1IXFDK=kqOM06g=j}H9^ky6b zRWb%%rCnZbcP7m|_l2j>q!%Gghke)AoCHuS(-~PuHTz&9E!l66$OP`C2bl90pA0=k z&!7Ww#~|=olv(4U*+WAbs;rU$g>Bu$zcI@(FRH+5_7n+Kr6AR3%L$b`77~h;E388~ zwm3RCNPR)IcCk#^F^$GdIUVCNG^lacJCRjUZ+0&82j!i)@hT2Ma*f)xmUcFSnV9;) z6K6qN-2O3Zzxu%WPqxF@nOn{gD|iF~h(8cQN-wpU5@mv3;J-wtB}NakD*O)ng8r#) z;3e0B%$*4U+M4mx>tTGapV&{<7uZE3q#&QAaf#lwIVkQwCR2bGJ935PNfOi)*$Jb6SGFoUc{jFg87~(Qf}c-tz&Sovi~9s>V6>rss@gpmbfFo z-o2tcx)Ngmb47I@UsQ=-%(f=t6zka2rr<{=J*3hU!yDp7>Nuezt{&)e+L# zTomycF>syj*|JkycPjmyJ%xf-0cTuVDW<{TiqcsJL1IN6cW{zO6)0Trt+ z{x31VJkU9BGQWWh||x1a=)=0UI)BTt`>2srQTp87Ao5aHXF z337Mx>7~j3=oN_g@KBY_)VNM}R?ZsMP~s?xsH^4NTuKt*i4w6Hun}GCamQ&8w&!YW zdV4_Pv8ICQT5o_*bi)4>E~yzS#3<~IJ_+Z%jzgaGD2@)*`yKrBzr7O%t!SR?r5@IL zbX+H&y5Z|PZ8sfaUFUhgTop!FuTxNFHJ&hb-t!<3y@Uih8%U1v_Zh!hmIi%1^30w9 z%4j}LtbG^G%^!vHALYoOnwEd;c{U6v2mCuDm>Hj7rC@bT>0R(y>~vUFnT7nUrHie@ zt|hJYN@A6?Es6dm(FCes74kQ?G%3~z9NiL?wsI1~r?E~XH=%JK5Dc)0$t!p0;TeUhgB zMgVNL0kkomn(=g8Je#$|)PSZ3ffV6DvgUgc{4&B;kxrbw?|IE7G5y7AM|g&KmI}JIfsEoj!=pyPFK#=JO@jzsV-hu;e!2fq~S_ivobrB z2YaabU;N#ZI!1nP?uN&|Uyzxtn7h4S^PT!%R9!r;hZ^i~gH+Jj;=;va`^gmxIo4%I z0>e(H;4TbT2%Bs1uFq?+E9Z3>*zRcgim|QV$(C$$=hyunQT)p_^#Vtjrx`?1u?lO1bW=J&*G*A-#$Nf^n zOIB=l`4>@X?apuj@?c!4eZGX0$^x^D(k@~=1(7c)4qvMUFRSgRQXtJlPcW?7Z6=C* z@lZLa^#AVT2@ZKS7eHS-ME?@Mcyw(YQ1PfBD^!X~Hq z9@ITN)U>m%KO;SRg5kt`Tk%Gc-NO>;*de$j?$IeCP;L}Ug{_O2`<&<(E)v|Z^0p%) z7_;D&(KGuw*GHE(^7;>~$0cVk+r#%z-z}~O0|&%IWuNX7hqbYRn z2kv$qC35>W&tizfG#pNQW|}%HZ0UA3ZchG(z%lsM$#PN2N!J6ved0@+!173)u{xAc*P+V2wd6bw1*#R{@PvN*f-nbs|t@NXdN}U_#IbyshWh4MsnKoqa|o z^M8QDGQR)M#x8fSWsuWNeNv&V((k0Ai&qc5Ypq{W;0IEOCy_dgHC&9?&y?(O8mOlk z(tbJ^#&X@-V8qP@kXHODN`Zk~Gk|v@SD9U&NT;{cZ>46(t^A)J@QU?&1WRj_Wd3@r zD>NCP-*_eAd=nA0WdIHbfWqb$?A^W{+4&eOa^t#?9?(Ih5J}#QVkaD=*?{fg5u2c% z!XPsbyB-95Z@J%OeYlyQc;&&Ap%C2)2F^JUajF9kIJx6C2ryg{6S2I3eQIDi z%5$Gf4d^?ocAo|PV9a!}rqX{f6#ZMV89(3rMOPivveMeNVR&~8*X45WMCU+ zq7gjF8r24fV?NN3mjiyyuWPYxN;*txiJJnc_Bt^5fHEUgV=8E zHgIX^>$-x9^6H{)C&|VXa#q+(W^RR7KkP|8(ep0**iC#I{4q}!bkLpPz8l!fh|mQ>j)y-7I@ zw@!XIPRa#;{R<4@rD^fZE5VtmTt@n&vInj|nNLnjrZ`>HeT>e_jy^*LL8h zm2EjfPl#MWwNSM@DfEVYb;8gyLwmmS0UF7K;IXSIPWZO8qkME(_qg+VCF|SNbgI`l z)HWwQ+1AyiodtN~D4YX0OFtP@zC=A?xdw7jdbvzCx-+97TzD(5=8nK%?b1{0aDnVf z$xA=E9v6Gdv~BN5)Eop!77nP~Z`j3xRBDcC#9Z)`w-`%NJY+~~WQTtxy|TLAF!H+W?nMSi77V78zPVKR-lgaz+&$deqbU#i-hF1TKy?-`OI~`5Sk`@P!C&<$5GPz9=wajsHseZghaa7cbKlz2`0xxhFC0YCU2?#tTGm7T zle(>F<)nm$HkWI7z>$`WkZ!C@YE@QdV@*=V1(<#~KpLoj0$T1a8|JnH}pq67`Jrt6rZv+&EFSsFIH4~&W=2b#Jzln zT{I%$f)S7NbED*EXL5Y+}5L6uXlxn~TQk+f{v7Gd9$c&Q2VEF!=P36$n2lQ;|{6CXu4Q}Ku zm>ut;*CVUA`#gS79K9R*SK}YVD3rwAlk?<7T;lu$!_6zeLit~YP#W$BNj7z?HmmlU zbC~45k|%BhdU(xSaUNVL4|!hJxVtsuH9P#Ce2Ehe(Kjq5zd2!t?SM)%fCT`?A3zsn zAW>t|TdYt*%`F>Vi2o_yx=epU{mo<_5tk2g?U;rI%HQ!=KtcQR-5DxQkKw&p>N6DJ z|LH|i4oIfi#m5&JNsAw1Q|0GuYwPlp^dIGwlxZILSlfx+ty1`yE8ou53K!5Z0xC&c zi0LZ_4}ZK!>8LRx^ClD;EJJ2?$Y;~qMIiPuU8_w+l%JTE5rEPy)58Fl zC>?aVZuR)s_Gc0qJOKqRM2*6q=9gmg+k!g8kD&Jru<4rX_S=_(2e<3AQ75J3BwPS6 z1UC{K5j1oNnYH+W_JmBDo1T7sPnW*8T?bhQ-Gq8m56xAYoRhjsYP*dr8gvknbO%!hh9%e7$+Ueu9?@3O_*LH`&>(5bYT*}ezSFM@O z*G*eejJ5)fbQ_OWN$QwXb#L!Q^`>RV5n^+6b$=VsIa4+vZ)l!qpHlJzDfDc%#SaEB zP5WvIY#udY4wWHRm{Ds?rn5ieAy5@N*2{Gb4S~n=Cx|K^DNp z9I4pXb(Y!oPeJbSr;Xb+lGFX=;P~hPJa+lYLFgxtlv_}LlfwqOT@8J&vROUTr3zdO z{59NPofUmoBXx6GCYreoHM>uPHOyzq7m=9HHxO0vCF1CX=ROjvCUkfqd`Cod1-Un` zHDmUXn~H<5`zl=Kdw8D5U`O5!uw`3hUqRLEA&qZN#%TsB4cUX4HHcf)iw}e7350l!|U!K5i6}oA)^VG|60Mt z&9$(83wp|>aDsA(9$4`E6kkq$=HLwh)kr$*WW5cBU`Qf_u{$p8!+>rdjzW$fg5L{W z`xPWcPV7^)>Kb7cCvI@r2d}AEkMI7O==&T77Bl9j5z|E+#jq z%}jC%Lq=H`Wy;}tUa;U`Ru-M9=sg$}9BzndGL6{YsEQBk#kec%W@fb@%>&0YO%5N{ zz;dGPoJR0eVt8pbdV8p^`^;&gAq+7%+W~$3y^WNMHS(7ubA6){7(+ejEJWnU$3^e%UZ7*4-ab0aetndHTUbAC{IKCn~j zIlY2VMiHl%qa}4D+;5<}J4NT5F_`ObQcJG)2get}u-d?w-?&5@bj2p}iUU7CUr3Q_ zXmCUQbZm6sl(!Ql2X3FABe1RXGeUagxoX8!o=GzBrj&UaW zXzn0M-ZF9|sGo2Wzh!(BZR7~Btaph_y{pO5)j*ZdUx&wj5OKT-LyW6g4$f&v@D0Q~ zjzL=SKc2GkD9Jy@1V1@&APItpR`h{Ms~FoEBaE=Z)>wEDx+6v|2!&>btKH_hav>l# zOBb|C!2(s8@2a=Xnkb>irM!$j(bnAs(iaZm-(8KYyVq>VwMOG034~1~-4#w;y?n6v z?vWr63IEYb2iO)BtdJMCc{3Wr4te7LOTnN;I;l?-+8k24B$$lv)d4)3RSuOk-Crd+ z%%@p%XIv(LNhk72|AnFR+5Zp4axRJ@*&Rx5mo9K|C8G5oxo$wK<*Y6RKsfgK}U zYF)rxsE%A_QcYlO2H!65z?kbs4(=}fme{;{I|ZumWY_ZE(B zmz$1!9Z6c9R&PRRAy%HKV)a7Nx~-kimNR>2f$NUHGNfP~f^dY#Ron=MA4@fD%oS3{ zH*cZYp7?bi#Oc)%78m{T7A#ht1&#i~Qo;Z(l??h|8W5cKICWp9>$hdNLlPdC@!)=@ z+LEFA=MwRxejSp9{n%ev-k80u{0*L2&H~44=I~BaGM2V2R!VBP5D0`r*9VG)rS7;n z1P;q7@Je=W?Vz!iS37aJV+2FJV}jvf&vik^yDD;*cc+$T0)v6c-cs}s`vo=;(+0vc zJeq*O6CRIkhYl@h10r8F6h3>H34pdti4OU9`{?oZ=gWAA1*cC;^$QVC?|ps4i@z5l z1dsBp6NRhIFl7O~F9<0&sSgR+68usdVwD3TnNC54lRAkS zcplu=p|4tI1brMyGE`CFiKYDXjRMG4cw*c%F^g|u6NCn2!z0oV6I^p?%k>*dUC(#8 zlsMwh-qcioE6Uqb9@_U1Jme$a{znQ?`%L{oWZAtNLQNP73bp8y-v_=-l)>(G;W-l+WHefq$~RlZW&CRY2TkN95_^3$m$SoS{8gnKp{ z|5V^~#ArAG8@ssS@P2Z$vM_A|9Pt0>H_~>(Y;G9B;ss&ggSH?&F7maECorx4>4sVM z?b@6#K*u5w8N049vQJwMsksqU0Fz|!SgqWNpX_>{gOq6AMYG0kGx)vbj zF5&75F>58`W!KLI*qrominZ{40|}_*}571Y9+i zcq!cxXDAe^EYp(cK1gbgzbQiJ;!f`TR)g~=*KW^q;3?D9d|FEO%bjWbSjzWZqz0Zs zVkYsEJ-d~MRONZJ@kVP;R;|mSX~-^I&$v)#9r!uH6)I%S0*{PaQ;vjm$MQ~Wto2>^ z*Yx0}C`CnB^Qi?FFbzWcO#J0>@0BqzU3ZvthBp78TAp?oT(Vok%kY6ar(-!G|A zoEI6pcU1THVRGyc$xWQo%oY_7x~wb?;--aw33_U|xG<1zY&o{v2pu#U4Cej31qUUA ziP0(GgQHYg}f}mRiC6OkIDOPvbKB>W|fXM;r-pee^%01{I@_0~P%>nkv zg1oTazAm$C;P&~2=hp=yp(^Mo3H+azaKvWs4JJz}h&sRpGp!DS_RC;WN(y^<1sdP+ zuSQdV-*G}XrT8DMcH;dRisHNB{Ed9^3G_o-*G&RG_+wG^^Lkb4N^KoWM=e$TAUZ z8}K>58jL8&Bmb<`bQPe@IxFoL?O5%enP1g`foGT7@&6%+D28#&DlWH5s80V2C5<{P zipp-_Dz%hqnzl9Tisk11ElfkXg|=qbfGo4-kROP?MUxAZZ4!)<1jJSzn2szXr;&elB)|Pvx$8-XCuKAmLy7 zGCs)U^lC~mcwqTCU{n=Bl3OjitKx9&!#~khwHc$3ny-)e? zX_o|2gDkhc;V1+?aKYZ=gy{z^;F`Jrhd1%o+1&8Fpg9%&Px2rSM6yD%rK{ilD9EF) zf!60G*rB7~h^La7$xCVm$zR2q`a?8!5stZjarcAgfOt^?i zOD32s3Dzn)pVr>ZwP>hyQbLc4=mt|Pw~?UZ3nPLsdFL%#YD+-YFDdjb|3y5#uO&sb z6APzm#4(c3S51Sf~DE#NT}|JqBB*kn_8cSTVDH@ zFu=W8=9$`VI`1&5_eWTxRypPo`oY?D2^uT}QYU)6@9L1uX-wV6rb_9lU&_FkN=Bd3 zC=H2@hfG70ny_pwk!TVpv&9vgO?}VxI(&1RhQm(B>5)q#)KDbKK?7JJ1f3{~Xp1K~ zcTwtG1}&~Qwj;_a^Nu)omyC5*gjX@M(&(TN1s>M60aLC zCT^$bF8FUp0I+v8bjBZMgms*^YqbS!VbTXRxN;>KA$*`y#(KD9weH*bEhSaj~v{ z*w+spk@M8DDf8klexv95vXWvT7e6}c`kOY71|YNZ4>n_tR4ejh-&<8u8N-sIGi9xl zc|4#BY3$l;=`B3y)&ftb!~v4}G7pTkOcZ2P7h#kGCs{V4T75mGMPVo-)Pvr_uddgT zt_$V%kN7k`X(3Y^}IepK$lt zcR71~ip*UX`x6cp4GW3W7$%mo8)Du9FL&Wmdk}ADd3V&Bu&RF1>Jwq-AEyw?nWzu`vQNjN001WKuyDciL6$`=zmaCLC!s#L0?$rZm%f;< zE#T1rpIL!v__u~geW4>18EnH0{&SOp@9(=nG2MlpAcvylgH5v1FCNLa5480e)wg*%g z268D|R5N~SKQL9^H@tk24SW@d!>K@2C9GggHg|e(hmAvoH=dU3;`ZTv!0}Gg6|!9Z zm;O>rN7kO$e-eW8OoMmU9M|n>8pHvWn5g<|{Z#d@SyL`)Sl8UB4_zir>BC`m>>EaE zE?kWSIup7G9^!2sl{=?j1IF6D=)0V0r6Dqpd}{sdnGJl1%h}Ti@qzg^Gp6!c@^ik* z6$xnMlsL8&ex51~yNK0VJ^R7LWFJX$yMCD0{aswD8oKTvMbQLhh7Nt~g0a~g$b z*yB-jFxR1v3DN_?@A1&Voo>y7$!M}+mYxq+j;p&Zm;_>1U!y;Pc?R!096oZdpBLGA zVP4A<&pve%Ru ziFf+rNu)G?T>3KQfBRsmF()?gK&7y3IcKa*1K(Rf+01uFizzCi{~VIu>iAmm&D9Ll z{aSB~YPq9NO!18&S)Lv)80#;N#p%p>gB0L$RM9_YL27%@y33rT&zu*!Cny2cSG*^v zCZ>i+;xwWTn@^0Ii~w``54bUt;cnCh&7qF#tXoCb2e(rK4KHZ#9C<-;DM_D(=>uU! zWiWc17%fquw(PW*MyU0Z`HQ|PKPTSMUCEZtt0!%(N?%iK$SyzT55CoRD|zuP@Oro7 z@EZcP3Tx=L2lZSn=UwFKZKxn;+k7&mIfF|C5K0n1ob;|V@Uo+VtCsk@}N=)?NLhffe2bk8WjDKM3iC zbBTmz75RUWz_KJ0Xy$WWd_28#xuclcET?Wb;43;#yCr;!&O~vOB7a=)5;LO#{QlhH ze4qB#9ly#!w0BxGgV3`a0GMzm^}SK$zYs+=qG8cANNa%g`E(m$jq0jBAjC8SZDAww zGc(Ccf-S^D`PBb9fS<|XblQu!Wznir{9djO_^!`tXURqld9^w?u1S&?4y^#nzcbs<~`gy zxosKTTAitGg>e_nKb_9$b1O0)Rg3=L)p)}8NPxP2as-%NGW~s)5M(J4Mxv7oc4YXc zZ@^;{=CWK?X*Z9J%t*UCA<0<*!Sc*SdFjdgS>H}TvDiEu?(*UMRYDV|Z!LN7p%&Xn zMvh|q@vRU79+Az`qg#6D!48{mxnw}A2p<|!K}N9$avzR9vSZG@-K$cHYEO*NcHA9r z?#-yAs2Px8&M8>jvCm#fvOWj^%@oZL9!`!U_88J(`<7E0z3wXDh*w03`aFpAUUvUF zVfG{2nOJ-O9H511A?brNXv=C4k zDn=QI(E|Wj5z}zzvr1o(#Js%36Q%TcRWwcNyN_+HdBPmdyksvOYVQ!xWbUl3v7Op3GwV+v+fk!*~rYgXUyL0-e}hjgBxJ zZL;)89yW<(0_?ah-q7n1#$KO0esAGs?=K`Jv)3`{WSpKTT|1QCKf7WEvmexHGy%VJ znp1b|!IW3nT5LY7S*O|Zg9w8$g7`p+Y_0hzC_wmNA@_0iZn%K78jYOxhXAPJiG}-- zB@2?kL{xxy049RwB?Bq6Kgb9x)KV92(|NF% zzp6G=WoW!niFs>DHBqKaG=587(|2Cwbm(VvXklZE^$J71?LPca$k1K@UAjHx+WVz{ zWd8u;_S3`alB$hBo)6yRtE*{H{-D%!RNy}jLA4)0s@|>r3$O61{w;aXqT7(QoX|OU zq*Feq?CL~K8+hAkAP{^==Ajoav;Qh{a^k!81R>>Sk`uvBE{x2HDNe*iN=EaYW!>&c zQDaDm^pP`Rg_w_F%s=b?Li`M0H>AVpDVRd!1nV3tqRO3-9S&fKs`X0v%e&0?e)osQp` z{B7_f`}!j2y`h*c@V?PSXo-{d=6p-{xcbMIGZ1EE3l{fHfQxtdB}N@wfhA3wX0W;u z_;3wV2TNRa=PnB0gnV}K2dQX5REkco{-u-(_P;ovChYl(k%-H)tYm;|ssS$GAdJkO ziv!j@?6{2C>5i_>;1w->M=%nW(m!g(@1143`&$&*a^?@t4@TM3XR){odJaOR)R_kI zQtqq=6}01759+UO?m#g%I8g(^Mk5@EAXe4(_qiD{5~GE^Y$Ws1Lb)ffDDM9Tuu&IS z!G@XhcN0pOnmUwcyp2Os_n38BDOO#sA$1tYtco(7-Vc!8hkiw8V>;+E&lT0>4xY{D-VaI|60GG>NLRC>+@LuE= z#)eMD3&m?zLer7Bw1F{Z*Jp>eNynbN-7OwK+E8U)-Hf%w@hK`9XwM)vV)=7EYLYva zSr$1_Et-MF6WnoUB!+QpzwlBX!1v{In!CcpE0FW>fV=jQ6|F3uL3#2oM2ba9iM-~l zy1~&IZ-p^|!k>as5QIK=aPd!c1YMoObl9a=hq`Kppe8B+vLGM~Z^gCenS$T#x{`@- z(pp)f&j8dt;;Vssf;LeVI0b*oGztaH`G%^Xx5TzRyEW+S>cZ{x!65ejhag(F6=4_j z7U&7QwK;oN^bBlMk&{C4y8cBIGx+hhZiapZub<^Sy}vSWwcG15|M2S`ktSMDz^CWk z(IU!~Y*)YPx6$&`BX#|}kATY{NZD!0!XH<$3`>C5pqE&Iban;i3YF_2MlXcbcQeJ1 zB6r2i5N-)jGkeHPdjV(m+zbDAXo>KY97lxI@o_vOH`5BO0$$ixhzIZw!9X) zB$_2BXM10VQz8l&sTAch=Rm2c2t_JdlK^Y6rZWNP$f||P@9j2N)1Xf)sX${Ho9!&+ z7f^CWj&q{b+s_Fw^Rd&{SWsALJ&~4mV?;p6U;rTtY|qO>?wnG?@7s%pc7{!Qfs)TR z@r-vRqJ}u%EF8v5#wDZ1%Bk#1Uv|62eL@<77%h zGK-3kUfUV|)fGf~dYuWN_2!GD2o2~X0?$-8s;fxXYVltyt}ZQYX?Ai|ZP<1*U&%T< zV_}+y%JTFyC{zHTEI~o$_7T;LZat(-mFFiVlc|kgg;fpn2`#607`MYq9N*iK*fyfy zoOdMfP-=J5oY8UHl;CVC1Npyb1#u)^ee`NK8S1X5II>nosMfl3z_f=Sn-|!0Wd2b$ z%&WQ^dSK;_7flT;N!%HApd&WnA&)j$!QstBP39D4CL1IsE3&x=m03e&xn(%@g4xT2P30PnGC3doXL%{`_nzQFNCggFFPwQMNrPLo{)CKP(i{BP z?H8zzbGxxSswu9iMfoj_5?<{fl@QBOmnAul5c zg9nt~Nt8^9Yzcg+O~=Jl>e>97;umBUh~|~P5yf;(gU11z?zcia2!A(?r_jgma{xBE z_X1E0oZbJU7OGBQUN_;k&Z%TrD?&}&O_8zI1?CcU5ek7m1*QXoQx_ul7OYg#d&)80 zIIXG>j_mg2T+1kEB-ACR`y%=#`q3^dz1OvqO>}feP>VD@JA3`o-++Mmo_dkw?!V2s z8(Ql2v>->$g0V!Br@gF#B^gIB#zJ$WnVSqQU=G`On%o3;tJw78?BWigcXbd~>Q`)U zDfx5}PbkQ`@5B)Ec!GFYx5ns!vv|F`i`sqR5v+c}^*X(B`MqHNuztq(eO13oBKi|- zWM>z)2tew5G%Ky=4aM2O@5Te7SP=W8)H)7NI@cz>a zt!KqcPGuosq35m>Aao&S)M0D1_3DpYH|%7zOoCo0_};y`JFtz=HNd?Bx{cYmNvAZH z;q;D5K4CQ?No*OF&}8O<0fnjMwt8X*@A^nAngNv%O2dDsfrhu|MBr?eMA$l0b6>YpK zzYCfpu&TeOmY3N%b>{_2#*o8}F!20Qv){4`acK{AtD5zs&)`N@nT1Qap9yYAjjV1| zz+>a(Ki}C=3&{Be4mPIcw1n4@+FBpBi`!7j@4CO`V~XWud+-h4!E&p)*FmE_I1VPL zXdCNGzfkBofD~3X?{^wsQvgJh3OCMw-iHx_T?zoc zpw6`JJ+%LXSitk?$eE^TUb1ckfBR=IYU{J18^$ea8btao!o)1O8)iE!F$Z0ux73iq z)YwI0$d)U(&)Wim_0HaDxp9JtLV%#_0$YStHx0gxpxJCtn{!w@dd07 zLhZv2J@ulI3%`Bs{Cw@y2YLd+!^9rZWrbTd{e|yC-&L@h1FpzqC)J*(yCcWN#tKfK z`}d=}sivT#3(;-m9oYLWhoWJ@q+ljh2U18b-I1q)#7K&yxK9H|v~dj?mXWw4u1|kI&|Sc=Vi{c+Htj>xZv2`9;ewJ1RFKt0_Avt#e5#6EQ!Uw#8g+2Dnn`(F808Q&9(=Zkc$k|p@{_jht1Ke2az)}IWA?hs40)&TM+~mlQ?R4Y zi9rP8d)g~Rhb)T%<|6RFv;tJFrv&`i26lcZorfW%9Q@^V=4|7hv~1&>wOLn#Er!*@ zx0~XQA7n0{tN+lY&}aFot>8Bs%GvfSc8hu6EH;!P81O^>0XHP(zhWV#QGOrF*;4;GCH(VR?qq;H$Jsi>gV-+Mq0Jc+LO zj{{O*>+};LDKI<->ecfnFT;zV<>|o8Cgvh8Vho{hv zLCjw+OyygvUjtIQ5`Pn9=KH6Pt#CyH+e>i?i*0sbQ~4&N0_!Nn8%eP_SdD z6ock)k(n=rBjqyl>O>AGlg*r-$cmXQXQ4Hly*oWjg}XnWMmRhC9pWqPR{c_p!m#<_ z0P1%tO}tP7ko6%DM4Vcp4`Gsd)xhg4PyiLb<)f<5@|o!2qs$9Lie;ND2`2 zI0|V0IRfbN4{_y*af}$~F*dFVg^lRl`WLbBogjGjirXipL?04cVEr<}Lk0iFrF+9i zv6gDd0;#swcN@1y&0VfHL9!* z>=RO!=}>juh^lsxiNFw&B#;Me?C31IBq(G(5w&Eb1uAY4XfW`6akOqiH7AA|LoSAB zF;TH(7_wS#ut5nPKY*4~;d-HkF4*+>K1Tjejw(IBz| z<7m?Tr|b#*WgvsVr1Zx&a%wv0WW=Nz5jR(nr<-6~2NeFirV`;16p;hLMpO4FF-{H+ ziDxc5D9YuVQS$uSxAA2)glq{EFCVzfS#GRK&x^M!cw=9+8wDwvoBaFGZ;CUAWS-G; zJ3{h90ZD~>Z=Fc1tvfbz#$Fgv+2zIV*8d+-_^pZ-LZ8M{!_6;Xco=oj+B@E*YoV8G z&|5aPBcEO&9Y`GBPI0rJun6L8ks!(y9c-A^Y!!P*Huo2kG0^ykJ6kYvC|!L)ob|4T zmqd%%#jQ3YPB8vIG?}XhYp)dc?9`y`nMqF66bC+P$hp9a{7m_21-K}eD2B;E+RR4^ z)or#a!;{~ei_03&wx8#-aoZV;&QfPX)tcd=nR z0c0{FbOQ8w-jRX}TtbI7YN;~Lw5!i@gYBRd2vyVxV0sS-uSXn1QzB{vs%wEcd7=%S zU%xAaDquFRt*g51J4fyickOWk7TWHvfTDtFiyXW$s?P5QJSh z(ePr5`yc%B>kY+6KVt6<7kD1$DqK)ZGykCEU=-w2k( z(n;HpIJXc=r_fD;bdz8i41hpi_#?Iu1*uU8$V$bwARdKHN=g4j5Q8hrXkwYtsqToXv zKx3~MN8LJ5K_KL80z>Xc1=UgV}RZs(m=tR`~F}(&(@vuJU4a>&#Q*w8P)VxdslP0msWvH(1U#5D7Lt#;x6sL>* zhAPw7)fCp=pi?FyRR(sbEyD=L=m?QJrHgS<`i$wMvkm+x@PHt$44nvI!B7M5b4FpaJgEiZ28#wh9I76hEv~Jn5hWrvl-K$^KH;zF=GEG$+<4qp0bGou0fRB@@00~ z;Yp}7MqP!a`q8hD$hP`4i@NH0$^3XJ5HNk!HP>nX(Yffod#J#Lq={Dq()|K#)Y{AR zY|pM(YFWnc9dX1j)8B*!I#IgVT!IcBBSll^yrW5L$hIWQZxi4=Vh^2QDrF5YW3-*p zAVG|pLsX>%07km3UY3h=^(nQ;rx2M*OUUT>?^s~mZFU&rV4qfETMj4|j*W`B1J?%c zZ)ILlSWY+Agli$b{KYt1~&rCuu+NuyB- z2an+zx+Nn4BvY%ARIG{G_3z*-H5VVf$bK|+j@QzEPWe1BnOlzDT7icuM@UMTF&z=t zvk8`V%kGJc4KCz+{fzWDmJVu2JVkG`(&fy^3A$W{HWq+HX5J#nWmT65xwYzSfnD2a z+=o!(RDr}Z#x8#fMDe|-Bw{O@4yGW=ppgZyAZK?CR_g-&SLvw=Tke@5W$~`8kT*?V z=qK)rOCdfoWzdCl`B<>!1gtQD=?ejex+qTu1jnlZX-58D8*tQ$nY?20zZQHhO z=Q-{D{XPE|U8}2a`k8ZGXJ*fyz2``pV6}v>p-8`jESNqRv0QhMVZ69#M81;l3wJ>G z!2AR8obN814kKKw{nexMRrt&*DvWzzGl@l zHO?6FFE=~8)Ugsjp0D*kN^wq*6M(2+^cFvdjq*~rJupZmu*?Uc>e_Rc(h}-_ z&xb#+ymJom+h%O+avTL@d}^@2SfFiIU{c1!3Ad9GRz%zv-*zE1!8lg4YwOkpI%jzK zFu$P7j&bh>G#QzK_3qp0DnV(TAS_(8cLqqVln)5x0cKHZ zej!EU!it2Kf6gm?r)CA#lQI=B@RR!G=3bO`5&vT!%JR%FVa<*-6uY>Jt+vesdyrxn z;f8-CLJmWk&!wN(H29sJcbEMFD+?oBGKyktH;9k}g9UUn%pL;q`AnbyJIj(7pDXcQ zk{4#aCopl5R6ek}AkwwEzuyViK7xnTo0v6EIMK=O2*{FZZOW2qwlKco7`Vl-3MnCA zXbe&$SvUa7O+5k;GA`2Q_LONwPP|R+>Tgsf<(*89Po?D`HOgiWE4vsfY@vNqoDTQ2 zIdJc#eN|g`Yy+T6f6jNb>k*utZE7ay41Z+Ebi^o}GR?oW6~!1)J?oL6zazLii2w4l zy7`ug*`AqK>)Qq^HR6EDg-J5I47^qMfJP0|L+tyB+1eF707GagOjB3?n_ViaPEysp zbxvRtM%IpWUWp^Vc=E)%AH_pr49caWIX64;p-gVlY-c)$te~)pO7>V(snr^6$E4t04k#jB-OiMADrEZ{K@_sXfL>Fl4gc_^<+L-kddxZb=>i;)&ENG}brjOa z<7R*?IP}A*QwBa&sBaGb&z1m9w6oy0s0w!utzAe}1&w^Ol(E4p{}konW&PZ2D=lHX z#vR8+=W3t&iPxuVf4#N83d6FVg=*pm@{3S?ZKH`dNaSq)#{sZ32{cbEB{36EQc&Aw z^>`UN4LQG4eqF=~c1tlPI!P>klu3hr)88req}H}gdz1>{$6h?pNAF*j3JlWeI#Of$ zO-2hSEy?2s^y?g@aOh`D`@T;sVYuI=ghj%R7EwC#T=Tp#(%7-yZ#9T+_MY3&o*S!z zKyMGj>&WjAd*K68+pdjv?C=U_-p_eA>S93zBOVFZ@rF1rhbsVm)3dgyH=-9HgoN-cFy$=jz{ByX?$DeJw9|u+%ww!o)1LUWr^{D!)OZD zF;;>tahmlr1^$3o6+PXFkzHt2KfRAfUbAC2`;(T4EnQX$zK=^Y8F&5UV4`QQad7; z$kbcXKZj~dNhR1^V%yuUHXl8X*|1Bj;HQ1THp^iuI{^_f0t0x+KtymoBUf9~hNB{@ zc%q+v(c^l!7IMZaEb#D093Wj?zr(=iU*uUY{P@GIID;__!u}=cfrbgt8P=-neflY1}5B$}3qgB_3LnrY#2~JzSV!nF6uvz>R_&0UP z3dLpyP!Hihued4`%|j?f>;S8gvqJ))9hf!f4z;^-sde{RFU^rY^2A8PAEbcbs|%ml z=rV2t)8oz}Xg7I4{(}Rrfgi4=;g41AW+wk%44nw{)MkhLYVrCR%1}h+v!c_RJ+W{x zR^g1`)~XoZPrAL@cN}k7>k=+aPV7sv2Jxqg(&e)qT+VO|6<%hR_ua{}EScwL)dUHv z52W9DkDcpqLtKYCARMV#Zqi&gk|y~Syg4S{d!iz;u$7iU-u5}pGr6bb$pb{0tAV|f z=?y0vs4WgK&J%Y3%kI;?iB}oT|QY4hhndcK7KXv4O)O68G03-yat)=XlxX zR`YX@Z1m%?oSoH9#0EH2m92Nh>Ek-)tq|V^z6M#kNKrX{g()J!zG-{2@%;p?0x594 z-Z65t(J)=zglExC{(CxXOs}?pwVX@RcMq h$}K{pHZ(k#ppM&xDr^c=|mFsq<}m z-E#}?AbuKGU0m^`WSQRao80J%%$W%}Qt_9NIr@!BUNZ$A$AMI&Ie3eEb5j?rlNOvK z4lS|a!1r+30-5$0M^UBh?rei^@qs;Uoz$B37fHh%n76b#K~izIJ-s zgg|h0Vpf;xmX4{2`DF-6a@%LWc_?gfn~#bq`zw82J1jLNY!WQfC|N64$b37n8*vq0 z>`#RU0ihTnBvKRmiEg>yPu?xT)p?T2VnTVNe`;K>e`;Li8*;Z8K>MC|amY!l&wd9k z$s*%zIji|sj6000f=G@g-!2~rmT%`}#au(?`Zp>WP3e8*peZ_pbf1U3ipPy!52E*;4zY!nV(v<#l7zl5gi z08F%2NkGp=GxUwo7m<}{Q%eh*Tiu1pAGh@nJVmYamcHrD!G-w6<=%gq3|%Z)WC2>S zl2Lky!XL)*i?_N0XVU9>nh_hPBu0r81g`bYLZHv}-ra;jnt8#W9{^+(K*)Cmx9!Ol znh^&^=lbsTLSWQz!$W`Ny{Eit_?Vk-$ObmMgn%m{;$ADU;Fn5`QtDZYfF7GtDj?vm zd6NW0%^p@h`+7-*ol*b%DKe@v0C#6#rCp{^(3c5VfcQ1DLF2LWNcdEDJXB$Nb_@$` z97uV5#+r|`M7|rk@lNhB13QSIw4tzA(TbD+LKF=x7$SAKz{ygW0m*Vy7C>-B`J0TUlWBisIw-KVDb>5sJxwC0*%NATH4R;!t zn;)n@dThI9*fr4irxNVKp{tBYR`zDeI%%^FMx}TC3wv%{t9*b$dyz4OQ&=@$fgnHP#}q8N{ckk-EvH*kPZKX z_mcz6kc`nfI_nsPVW~Q5Brru|1696-NDi($;n5z#O>qZ{?_SVs%JCAX<%t;g(kl&n zQh;&(N~Joh(u7%$;-C=aIW1lD6ng|> z{B)DU+H}D~ym*LU4i_(Gw$$K|tBMgMC0LK}Q)r5R!a|r9!+A#oD=n)+3%ayo6O^W7 zrRQaTk6PT+X&G{(ENBO0q9sPEg0_ECVV)E2V-zad7<(JwMsbPB5!L(gL7vj~EG|20 z`qWlw{cvEZc3lxa^{1f3u_7^CN9t0!k5O4%22tDsEA4j>XcTVO}2}wXH-j5laH9CxJ z&FowRBZK~9VI3nrhXeRk0sBViKaA?3;)k`<9wi3-=E*8}v$|FbiACfGqy>1-|5?_` zh~!Tgx$vJwnWNb)$_eA?tDC^=IITzAs74x064-NMK7Tzjc1uc?l+=)r?v}whNue%A zw@1Cy)Jvh^yp@3})xco%*{mE$?^f;>)Wa>v7VSHS{@^TPdV|L z`i*AgBH}~_(1_Ogt^=uNxIEh(tMV}n8;PNmdea?1lK2FX z#uDUuJ%E4Pf)%qRAp(3E2Vjz&orK~o%3tcxU}Q7>@|rE`!T2&^<})Zv3R%6bR3CZo z7k;TwVN$q#bJqXoMpH0vVdefeW}1Z{g!UmKw|%>~bRj4?KISTe(`bArdXDz4ExAeH zkrdR0Ha52El)}I{sl!;zWGr#!oetYBYediyp~?(|ccF%h<;Zr%nj)+w0*nJI`FX>C zsCID#oCK6}Yfg7?)dsjd@!&CMp2+5QThDQrS7DEory_67A{ut}EEZBD3Q`raL8 z6u;D)8sWAu5j|~NZ*(~CWA@IhUGj);x*!{UKhm>$8m7=T-IS39?UxhW7Zv)H3wQZc zt$fIR3;&0$R8;};5i!1?)j97w#V5K%6-T72NzpNBlWBfB$qNo7KB$nYHsgc6|7O1E zi~J1TRr4Sn@vkWlWSI zk*I69*YtAU_PvdCAa|guXO=8h<-&RhwTKI=UoH&pNUIX$;m?^6AD1GV&Sm28>?CrK zic!S{LkUS~=?7QQ#5>_L@!MK_X7Ydee{8b-1M*67tbmLShyTfRlI<#dc~yZ+AoGrj znKNw61#l2vQeR--dHT&Gaq+El_KPh&(n!qns=#&Q*?!A)O@ z1)4JIuT<+*y$@M|sQP{tc%;kC)H?$rmU5lIZ0PzwEx>(V;}7e@(wOKz!3P6?JQ6W} z2J<9*k#>;7DmVGtV#4_^L*WA^whU5Y)l221&a9qs(#iR-aYB#Wl6wEFk*)h2Fa0yA zPLm~a$w9ir_H}&q_uNLy_MlU}BVIgu( zXK}{?O?($VN205LWmqrz5CrpHu`aDMWuF=w)%63BGO+S@%5t(aT`(e)IsW_NrkMa# zx~)PhHN6O|zsB~3r3;4&p}Pf^xCK;ET%|AS9Mq2<>dDy$I~^5YPTN}AHOrosvYH%^ zOknn%>+~|?v@<4tW3_UHP&DBsFQJ?S_Fq>g6ExL)ARWqpq^`0l8Ye@_k1;ak?hE!- z@fP6rC1wO$aO0i)^7EfxFF4C!9E*|hFU&xyb3G>Dx#!bcM&B@!@b*JffX7VoEOYul zzuh&FvpV?nQ0!rf{xGr=8jwM+p2|5&=_bO8D-42YaF$s#2aauF8| zjtxaoPS7|^4A{)9{xA}b1Dj{tgfP|`1RZ{j#H(%WJmbP`nPrN9Tv|XWAW&zYlee~Fs9gcVy-b6u%WH5w*=f;gs{G%^~5 z_=33(k(Vx5Bjj+~J#&R)oPJSXTf}w{H_tfts)bKgF;bF9KZ*WVuN(x{^4Ta?xFrY;#V?E-!RT6`g zTPCDhu1S1-3O=$%q6Qq(`5&E%j;D*{@z5(s{Jzn^ETi4J_>yvwUg&qLxw#XL+3!R~ z#oyZ1EA=lo96Dp74hTb&vh!d}Y<2BGE$|xjDA?(aP1@rh2Cq2T?hR4M^fnmPx(TD6 zPh^7Et_EluNCQ0P=?EaY7{rJIze6hKjLTcThTym=KaK0eUxLO_Mu(H=oH=5_`UMAl zS}ac@n417oH0K1_m`lmR`5Y~g#1`}u5Vgl7S^sH!SdZL)-+__O!hG=P>-^W~1eYpE zSadHf>M8pDJR@o|vjs_CBNd>tc;{KcMPJ;ft@r|}dMf`MZ>{Le8W)ovQ=`7-p8?+ESV0iiOPCOU3K`oJjx>~0sk(&SD)U{?wc1WgRZ z`7LyH9#s28Low&EqUmG=3eX#KtZig02FCtN6vJFH{JL|sP3oZ!jrVPLI-u+JAT@^Q zVxTfxGDvOo&D|z*iVNiuH?~XnFhvvIRgE_ZR-(Y58tA1nq63%=&9@!ea z|Fa-yB*N_@VjO+WAHypDSBbuT)YOh{pf?)GEk@U9ekyhCFukT`cS5T9MK{~0h`C0nm12F4mKULkz1YNgtxNtqTpkv!3IxhCSx8XESPm8LwJXZARgg=mf; zBTO~;e+S}6yDJ%>2al{7+!l|preI*(Ok8G*O|j(m_dJlc7WLFdZo|cuR>=~+7X%na z-9F-YVD7@6;g4NF{@6+am0_6zAaFxmQK7Q^vGfwgDr=MH?rfn)wg-9 zXijr!()`cW61~Td2C&6sWIvJ(x8SJKaqsX~i&%l*R=k^1H!f4<_%9Mj@p4}p!83zjE-N=iI@Ro$}h#oWZI0}g?N zs5M$NZu$DI9}uKA;=lAk*ZQ~Q%46V<>+vd1mi^y>02ZJ;uMKrtD!XlUS-$m>uEMl$ z@ieibAVa<2po9W{9M>RJs#j(|n2qOjYksJ~1BpBB6~R5&$w?Y?$RifXb#25WGQ-#{ zhKvPKmf~uaVI_BY)hWX@+d56OCK z*T#qAsroL_xKWkAqRCTAP_`&LfIYSPU^g_bDH}^5BJ!Xpqw{E>Gu|;PqvHwrUI|e7 zxqq{>1QxY`e|a_WR#?ml&s71uj+-1GCY<`|I-tJa50-3xnUxh>hk!k%%7JskjBi7) ztPdew!UIla5lD&Nx;w5C(_~7nI`r8LtRIZ99n$*k;z*>C;b#3>;h=$} zFc2fAzpy88rwS{?&jxaWpIIr5Kc_%&9?zEbiq1k9b#qf9CJ-Z-`e`L75Th^bA&Vkz zl^A39Y#teXW%}nvUZRlg0)(Gtng{4H9b3K2#zXiOo97f9RD7oA4*use(y0=u2C`}-F! zn@EPMq0W;(pwj!R)RSedv*PT+#R-#CR53_n#drWYmHkS(gntg#A7c1{B|Y_J-M>qc zS%%ILQxeL~$RPa@Cv`0do-=y$dKOfM@s1?lHYlM0wr?JJu7lI^^wifFFR$E}hb3~C zy-CQni>oZ=KkvjK=H>|rZ71&qdVDP~Wvm)xQG-(!`67JNx#RSS81P!!O|d?56^*}J z>vA6d517#Ad40gc$YqrR&?Ep6@BHTI_ou=u`rmx8z9Qa)@}JEWq%|nSFMRZQb`n54 zYy|w_m%-7(aNkv$PwzaYV&3#`+4)3Mx!%$W8-Ik18vgdefUCoW+W9B7&Gls)`|`NB z!#6wYh5L2w>qf7O{2P_-mk-rex2=)wov0hVuB8)XlC0|ldv-LHKi~ib`uPKtrqcOJ zS&NOq5mf8;n86nMdyug)UUiUSCCCOu83^YzY+%tiu%^zENWkj@~jSRTi2HkU9VVT@E!1KPfM+pa9VtXlnUO zON^jA1ou^fiI}>#Ke{}3H)hLU!LX$fo;glfIsnT8veHm>A?wm>pB}pB?NH(w>{jdn zzE-p=Ye2u(glY`DNMoDfYpcK?US`CUR6z0U zX0|2G{D+I=6SK*e)y!GhNl3~1HD?)$E6zc)9uq<}sUO>`y>DDBw|sKC&wMtFccdAY zw(i>Wdp%EK`K}+!KU3gc64qM;Yrxikx#{V#A6_GJcx?|{1um>HjUq}L0(noo`H1Lu zrm$Ufb>9OhRbRSr!lA*0!%1T`fQ^IVV+gQ=baz#~_PyT&tQh-Mtywcdt?>UkRN8{bo)Pc^(M1 zRWKxfGO&s?#1d#4oKx7SBY-WjPR{NH)bshS_y-9C?_G)!4=XTLmpFv*@@iyfb#m&g zb3uwMuwQi90On1{*RJ;9EsFCeBK}I{h$Zem9#6`ux|n_*vb_Q>*~ZURh0hjB^{%Wr zfDy?7kDDvsPt@=D+hfkwVS`XNn;3&tbSSd6z({#_{+m%8;79$d(jY0dsdjdMrDDwm zoUh7cT)GzkGqXClq79lwZO!`ydWN0FCGqF(NbISN>phwlbaq)Js46a?H8(T>jILLO zve4uI1Ixl-Z4V;#O|*=5!VMq-bG?8&vhhm}`FD_6Pgw!*>uXck1|Fo-nlM5e@CkO) z$usuh&x~b@g(R+krryLO+9G%t2e0K~0ANReakGQr|MM*94|x!vRVmV6RI@}ZeDQ5D z;T7f-*y^3(QP`F=)pe!2HmitRES{#n>t1|hA=nXfWma%FMv}Ny*Zm&hUd|gLg0=IAY(x% zn|00J)u`jkFpuQan~e=er~g%6)v&;55hhiu&0a?EuWxCGjKI^$9s1r`o@R!TjmTa~ z1+~WGT}>Wvpv7v|kX#%A(m?AhpJz0YJaUWh2Xu%f76Fh-C|*cclm0(@0_la&zPYlg z#<~$9>rgCnFI8!-;DEOrUj;a27M_N>w+=8}Wd@KGkdM(ga}+s+zDxcJFj(HSa>EHX z<)zqjR@Mhto`R9RI7-s|axVVsl0yiIf;F*hmaJ>6L@Nz=)uy%{AwYuDx{~`0$eG(> zOUg;O8Cu~YdyPLoDP=48jZgQQSAsC1lz}YF*waV0L5oPM?h!}?T?Xv2%zLoAGh zA3}?=pi#cz2zeS99dZ4S07(>9!d$=i!o=swJqJBT2j#qp4er?C;9r0Wo#Z5szPE zua_&`om`(?g3#sDJDaahfUFy&_<-5covttGihg*d7{{IykGM34=rMyj7+v4e3~M-~ zKCDJsh0hvndLJt1uS5YGd&ixFJVUPoHdbWQ@&+AG#s@dsNlH^`BdA20id!J2-er=K z`g}&41M%JH0rhY6ilFN+AQ>W6S{?JikM_Fvz^5Ag zkM?C&m3eu`4peiegqAzJkMx9IKAlz6&s#I_QmfCojH7iJsatDT`k=g6@$ly`{2z<7wlW6eYg-qYiCULRT> zrV?KCD$_sZEq1kDa>m6IJmOt)Qb< zj}bO+{M<98I$E60^5-|E$y&)*6`pQHmOA!vVb#bZzrs$$kY(=+;!s`4bC|kLfs!mF z!MU8A(AU=|sVXr1bJ~=)cSiJ0s94y`ckEmX8&T+(xi`;wsf3tJ{CA1=WQrY8DBb?k zJr@PW{qvVJZ5k3Xva%mz;7lV4u*4awvJ}oUSETJt+T@DSUBpY#sV5!3%Iz1@9&;OV z3uAynbLb3cu<)-L&|5kQY`(-RA2eRpiT|SW!AEcD|7~Qbeo94C7@{bn>#M7=E}Qeb z8AcG4Z?NL!f}Sw^gXZKYEwWg}dGmWKbhXL}t$>GE|z| zWo*A!%k{BUDij$JLZ{`P9uMkQRAV#Vl^EGsKy47>Ko+zSHKYb&qZhtckN^5!`A!g3 z(XwUUIsrzgDoZdCtiYM4?rp!3yeSNdimI5Ytb%C$(lG)}?V=@j8t#Cw{W!TCilIU4 zPS~gI|5YyGVURK|qpkazNHu*v=vZNaO@Z%sgi^;L8}&{WP* z1}(jnx}t8Mf`zv|O*n zjfq?~u>$dlLd&?D3ZYAifl z6#8Yux$yS>fwpUzlF63jY+}PjH#%^bHOhwSOht;?t-*%d_cmZ=ppEIoAwAU2=&t84 z4+IwIj;kFqL7@5mMVJUDlq9FeuV}oyjKZqYK%hnSB3%JJ)Zz0WV;e4(>VAjOzDj6K zhp-8(z$V2%Jl^}L#QczY%1ppDMIV^iSfzF$VU)@EGrLM;e7yb5VIX7wKK+NUlicb} zw@!@Fb9dX96Z>4>(9l7$<%bUQkzh|fO7s>HVxYKHrCRDci6#MLyMLC@*-iveQ#o7qAcg5Dz858= zi`#F}4==$U?vXiiZNi-e%r`=Zmmm3M7N~Ggc;L=T)%swCgSJ74e(}#r5<1;*hw_%6 zo}6;mmgj#($ADH@aHs;a#m1O}grvlXS2vz91D0@gn_mhu`t{CoQ*~O3Qgli`v2W*7 z?-@2FGW1uDWSfd1C!eZ2Rq&_>*xi*LyC%uVr{`X4@2;yw@`1|n8i`jmbljd zgQtj;_Uq^hX)Rfuci0gRoE!&%HW1aD#TwFVls0>Wu8Oa*srcL7GdxSxm9KDr!+H+W zA#`U6GCXDt9~!BV_j4sEXw=%YKx=`mABJ8*-mxdFSUOC@+n-$HYaJ&9;nBh~y&R2x zn4DoX-aMpT9sfF^H}yLC3T_AWAZ!n{+Z>U15Sz|lt$jSy+ya*ua4`ZMBjdXeEaPj- z4;NAn`lAQyT~mI+#MVFQ9a~eNz8_=GPvS#5h36xn1#7TLK~PlGm^5y!ik!ovZ$4}- zXsC8oRv!h(FniX*o~H?v`?YFUS2K3*{`40Hu~Kfx9Sr!_CfnpR^ptT?$Rm*GjC>U_ zXUD@#MurXbzcLmv+hB&5(rb1V%+1Bm87DERS7l>_ELRcdaNTz;A6Gk+YOf$$;kjmi z;F0Qa{CRa^Z^W+lIXQToH~PpvmED|<+|W6heI|O73+cD4lLiX ziHiL1^vIO>7fF3I0AqD@hd>(;n94J>i&n@-l zgVg)nysijQfHx^wO52q+H1l}4N*@T59;g%}lkk!l%IXR-I*?MDhYGpwJX;7~pyp;k zOJa0e9eI&O6k9oP^Y&?SI?mv-2p&O%HeSn|u|g+M;Smg}ySLVVx;kufEeHGrjILKZ z-_96SA;WvK|E=_TF7=yXUmnl*bHsd8k?uohMA0QyT<2D(5~Muql~_ z*hXIm0ktPxMSeR@;^3YR!m?U>)vBG@dh*Se35(BiF>G`$kgeW_wkG7;_mhFI{KY%Y zwt`oP;|M#dc(!)YSXw+%21sz5dDVQD9iyN^~qo*p$?7m*YuCSN?dXv{g4Jc z4#JLb0_o@1;~}*j2YV0p@qYKuu3viU-ni&3`27xHI!)>NpnZMiGz>_>*g*A%Osm4z z(q|$#|E#FtGsm=W_-4&DD=`*);<#H~VWPl@<>|HY&DDHDzS?vIn`_bwD&`?Gb}vkq z_fq7w|K|r5(}fo_w1-?iIlE%*l?sj7?fd@ndvFi**PhRAk(dmPt>q*!r=tTjroEM5 zJJRIVP;nqJQJ$j^k5K5acGWkkr>y7^duk-k(BY6*NdoN;>j-0Q69xvSbqEw(?H_L! zJ{#U4fAjFND4LP|J)D$u#N~2MsjcBPeNC8T-9?$sn>Md9A>&g(4NV`)vcur@*X{kL z4xy4861+5Z#h8?NnJoEc#*+%{T3B}Whi~^J{lU>am}XxsuEBU7+Cf!4Tx%7yc;$1z zZ+42~WLHIh8xgC(-}|71tqD8pekLqf{_&I+m(@aw-XbS2_};7=#>!bZ%6vU#L{kXu z=Kva>4|+a-uxa59SJt8`(5uTJb|t9FH@F|Oa2iSz&Nu=ch9(-T0VnPX+7l7Hzmm@=%%uM9l4V#I6hXFo-sd zFv4cQi5|cPuSBrUM88hlX7R`NfHyH@m}%IXd|h9CZQ^Jj+b76w0rjzaMTQyg1@$7c zSNN&>v3OPSZz5DZd+CUHT%@j-I31755IoTR%v?#S7@e!lsL$d)*8CG<_p?o0EU=Sc z8NegMV$y$a5SP6`;x+D557Wu;La@on-%RN8LxZfE&KW%+SwRYU1i38%-Lh`-b!8Lb zH0U^dkd@nV@8SKcoq5Y=V>rVQKQlFYDa0r3qi-F0Cw+dr9MA7R%igo{+>MV&r|7lp zq3Ak_P2(amg+K8#e()csE}EoeyvM^{%95dN_7#ft4#RL(C_ zKMhVtoqH;j4JP0v`9^mza~fp?yebR1e-fQapz}&S^+${(WV1wCf~vZ{lVD|`{XB>b z*AcTDA#KFBP{%XLAB&>B?HBn!EkIUaM^iROI%i7B5<%#xD3v|>4d+Bo>B~@aMu_fc zPDXmtSfp69&gbPj>!9N^|2)+WSKQkV^j@rYSVx=Ui)}q?|Ejt#TB%&Ih z2Ge116tSlA6-uL&&h}4GaboXzeSU}0-`^y0N+thREprOB{4lgecUKId!?eCuE5*dX zIa48!c;VCrBX$KWs7c~yV*d&a*8X9kQ$^~6=?W~s`CKIr z_I{mw7`<3H3TT9@zVo>q%_d!JN3QOAm}=Z3si_uXbuMsv> z7sFzd>CR8y@&#+F3*6pDkxEr8S(hsrSD346E8N4`F8#ngT8%*>K#^0l zc!=&WxVzA~Rzd`uJ8suFxY@>Yx}9sw{49Aleeavi?k(|%P8)z|kThv)6Os!ND@f}0 zV9?MHe&e(RD;$tEzRklorm7aI>?0z0o@I}qI{bsA+ubW!LY{{rBYa$%r0bLPl*QF1 zAL;uzv9h7PUcbG*6fu54e7pCVVEviZey#k}!w4{R^!5VW1U3*2K=u#=qhA=thBs4A zUQa@dMCbg{ljJr3L{ti$7*yQReVdq_tNm)QHZgF7$Wq-hbj)8(=Z=B#GZ`@7iqMOF z@h3~i-w-u;XX9%9mdpNGP_f(+gb@#)6Z&1fL<(GK$;b9_kuzfP6DsVxrK|SDFAs=4 zL%71x(AP<6%_fjsuAWJ}dZ8)2<9e!=#tn`KzA=c$@0yH>z_MJ9ky8x}Uhr>FNzzK| zzrS~m{mSEvCapQauUSBE{D`tz3AM=(wT^u^p#VsT4>U$awap09 zR`d%YQ2FHnWJqc97I&9j{9S1Y*A?sO)PE1m<~yadNm3HZx!3tT%zxk&95IgYGO+oi znRFvl)D5Q{cy-Tz4uQF2S%PK4O5OfMg5|(KGKxi&w9PHeGQW@ zw?5YXs7vytDI&@f%JYl4(_R=;xyY%DYY0h_r?d*0LS}w#c10nI<6^nkKWUi66t%hN zu;DYb^mU@5#H0jvKQ401gy`wtFQB5mjJRiEwERD&W5vJ}btRZ6c5R{!kI`-owTP(a z){smOQ3%>A%Um2%`i9@4VK1X7rA)?Sd@PofQsey8?QSAE;y<~RL#&QkP`WPIKN7EF zwi-di06Cm$`N8KaBSt86{@t&H34NPW3T3Kv89%Tw^t#?2u4*p$pRN9M+&w7?ea zcK@tw-hw0cpKru3!zwuebVa>y<9mpt+2yyvxy$M8uSj5L9t*+A~Y^*@HIxY_zcX8AIpyz!Oiq26z&im)1< z41gC?hi@VMUWjdx4g%4J+h9_qCxV}#bpq);`R#&{1n-*7bWK6dBM#RS&fBw#2wj(a8?#xTXTchh1 z5_yEhD!wT$W((G_`^>hs*HewCf9zi&uCKl5gFXi zyHGF~+mA9C>aE=NJ&x|gN7@@crL<;ZLq)c z<3TpUM!OqLq%zXxW(ympR&OHrM3fTIiYN39uHk!wqw~EMIAxh>EmHy33hF}p;fSF- z+dnikw1iR2*$e&Xkpu|uccCM~P$|6G#$9vaJ|$6m6aES_=W8|4by+{j5DcY8m>pG2 zpkbC%qS;5VsmD%?&JFmUEDj?j4t9CowBV}w8>>2YS{ z4s2f5k0i9Xmgq=$ezH6|34%-w*82j@S7|zl1*|1!Z$nhol>Y!XGLL2mh4_%K3;wW{ z1yBzx_0oq>0zrIKesYdT5Y{3R!*AlU7Fot+ji| zZ#i{n=l8)Ax_^q=-sZsgsh5|Lo!mh;%+n+sVzOh_XE64XCFA?RhZs^TMAbQ@(F`z) zfAq+vKWeO<+rr)IZG2SUlfRtcO1Cmuue~vpEWb$`{VSWH$4Q4x3XIrGg4O}_{Wkia zIHJBsP<8e7Gy?Mn89lcFASkLh1Uh4EtHe;ceI7yt>%q zql6YXcAuD4RT8C(zFEwsa>3bo&hP47)<_)wxhOH z>JV*wAo@I^N>fLuh^@UO#HxhVeeEWckqMZv;r`x9o3I4Q7e7vu;mkc83u%Il#myJ9 zwWqYg8JTSZZAO-^cr^|ZH2cKwwV$PX5nIU)D)3(~qi9d-M1sQBma74x%?T=p!zP5y zRlmuvI(mAV04|J4L93Nf`RdQ+!#h>;H>N>R9|oGq99mFU9j2VAhZQr6*X+qPcU12S z+u8S=W0S)^jPTO(5t9rh2hqIQST#J#TeM-ww+xzquwQQo5JfP@O7g zj3Xb&Jwv3aEo2i|fwC-((MY8Bm}qd@aN9-bcBKDes^k!@??FE|;10(7mhwoTKxhWA z9=CjSh_hLJyM4rekAuv6(#hSwUZ$7Z%Yc6Mbrd)>EZ%bkQ1liKgYw==E2~u`23M4{ z4!zlazr17xnKeP7+4%`s5vGM>zYJOrh``s6idFO9-id@@x(^X%Nbk~)Vsd0?&mAcD zA8e0p9lH74tI`Qpv?l!u86OCJr-5c_AV49<^hVIqucH5A4BddfsY%s|$m;Yo0OLsH zJF;LL_Vg=p1Y11Ei&!ND9pIGiI9z{*#rVMK{pfgg#rf}jTC?2v&>4-%{u>(VfRoKF z1kijM3_W=zy5a!oekfIXA5Vi2J*@HZQ1Tn~_O?;60zx$uri?6in(uQnx{E5eY`{Jbko#ok;IN)4DLzr=w%dVaiYe6-8PprZn$ zaA42g&o76?MNHirHJKm3pwJ65+10^?zn#V{t71Sh+G^a>!nhD8b`;&Anv}0bRlwAp z{n?Ad;8pD<^E*@}NjskgI7GQUVJv_FSUxu6X(Khp5?357%;9chZIeD>bGr&*g|#j} z`I)Ceq)Uf)>xoc!ih+lJ@Z!5TG2DT5=KhKN<-@lB#bH^P`;DK|~jy+!`E1)u> zZ+81l;2WcMXrC|C1-==8O&?dQIt$OrmLAolnHYv4{XK8_F=l$UIYB~*&*!m$h8_yZ zr{QS(2XG@!gCYRsY#-) zm{8&gRyoB(x968PvbRBzj;N&qzCp6{@9Yf^fXaB;xwQhv;0lAZkt7AzkswBTMF|%M zKY0Tm%=x*Ucx=kg_rhfg>)pi+@h$H4!{$IpQ?6v{Av^EWB{c9))vMf-Xs_zm6RLfB zH}(}$O}^LDzpV7NwFtn5-3}*#2EOuiJ~jg4-s6-CeKUw)-`0B^!Zi_189S#8&z~^H z`?n?ZHdjd*IDn>&(b1j15Lm`b%aNsL2#(yVtKTjBgXf}2ITl29XHTk{;r@|q9y|QN zHZDljA4}&Y;UV^6LrfZs-!nzSFs2@a=2o5fWA4!Y%0B6<6rliy5+=&JMYC4(NA>p` z9$8&?c;E&s-98rxPJvDOzic89La_T3LMYlm^#x5R$ZYuPE;qhpm$7%Bc@mI*_yp8n zb|miw8drf%JnW8x-%Keo76SUVuK*6%>-P8TEo$tx3r6mlnzVg_>PJ>;+TbU5c@M4pDL=E?lT&Pn!$P-qU($tv-GsnBW3h>YB*O`TUwKM*I?7 za?G}HPpats^WtC3nH=cIJEBd6r4hCZOM_!ZEY5_5p21;_Cfi&I;nLS05oJAuv;gN= zNsQhSyB9)`<(%s6@(i6rha||96WdCqe7#p_R+xaaCc<=}I4+k*;*4G}uxB5S7%B3S z3wbZ(6l4vMbz+pDl`NpTK|sBI?B>4?ut7KZ#pfrNASEp^uDu(l8MJ#VMw5o0dp@B4 z=JMY`%;(aWZG7;HhI}#R4tAOz5z}-o_Ify}FROS_-8RT@W&TuuHoZJ3y1Mxy)4L`~ zwXG{|bqW5&FK#*T-w#GcZ)x}x4aEhp@xjCa#}-Mvh_}F#1kshz3DT8GH7VK8P0q!R zr=eoXOK3)DYlI=BCCK{BB@$vRNv*g%G@o?g0)WXD8Q&@;mTYmuXE3rirujZsPOX9u z$^>}s-rz5yO(_ivy9*`S!y`Mh)gK;~7MJ5Lr@%DA_*4770DB$6Z-O44zJV`^h(~y7 z?kDa)CDwU<*Ph=lUb(%F=VJ`&<;JKT5dxbdm*z5m&s50F z<={`LEn9)T%YPM^>7Jip`jpCgX0`C8|BzOHw7 zAS6lam?dKrH>mn-PazTNekT%&kHG|Y4^s;iRN3|mnkjfz=FUDjF){U228X{smGg0g z(kuGqko;c8l?AfliE6f0I}V8i1U z4*iFIognpcs4(#y-o1P}HGdeeZg&}=t@XyG^ zR9jNGpv&+4E|L1n9aGDgPwp|>g^aLvmcGg&@@96!ED~(gRbMFP-h1|Q{8z{%*^Z`r z*8Q&RoX9k?57gVZ64Y$?lyRLQ6ybs@6yZTLqlN}2R0M(3avA>NFAh$+V}yES7;}m< zKcC_)Woz>AjD_s;5#X-?Ch~5~!QlMI?-0R?Ij_8J9#`@;YtFm6T~r*7pw>mz09i^> zZ{Yh25#{g!OQfxfIos;bGIHJf?haZ``s6hZ1W)gks5q+p^N+N{^j7X(ay%@+EJyLN zTP5YZAeXDwAYrpx#tW9Q+&q0YMAkXT`jZN5wKP5aT(^1i$XQNE+I|izHW1*iM+p<2 zBq+hlf%K$gfmo~z3xt|ZuZ2k1pZ+OzH8ceDTbPpDTMBGNuh-i!+W=;|98hkXW@`$BXfd4S}rjv+sYN*0OoWS8XZ&Rz zpsXy_N6^d6PfH|f4E&yoBFNY_o>>$Db@Q;c>fJb+gxbT({tu0A`I9Vzy(si>wKueI ziH40|eM_|@J0HN5NA(R(h7u-pP$d19Qlb(RV_ct-91+>jGvL9yy(H?SD9JY{ca?VWj$;Qka_}3|p z$!H`9n4Gjx1zihbDbOHkQG7e+b|Itz?ixdR&}&Xk6E$UtgrZoxhla<4e5nwIxFE+DUOPn7>`rOhQ&>DI|rvfRq_!%Lz5Qy7T2H&I&NWBGusX~p&4+ieMPiyXNFs&z7hMzTn)P&M zIgjk72nl|jMMmCR9XY_Bwmq80$g{3__O>tdOSLUg_9ow9R0Ap5#*&cq8g5N;MocnZ z#Nr|ZHw4*$*9UIS*}~YryUL-te@wSueX0_{3LNnRHNBN5 z>+&mwCxfnc(&pkE8*Vf9u6^M1sHzOg$=j|7?a=#XFCV75|9Q5sD=Q@wI->2>H!Ea# zA`S^*)Z`ZR0+Eq9NMibOk(g8SHQ{uo(qnq3op#vvZze2W6&vD|`8M0rTILH>=|_No zG%C;~2IQGtYJY*W+nHK=fWbQR+nydNGXr9u=N`ydJ8F#Pjz5~WUeh;Y-0CNggr18A z1HYS8o6gl@x~{G~zQ8HRehyuF4$-3U^KjDr+2lA-Qhf2_pD{8h2|UfJNA2a9RtLck z`JJ=)0auc(!!j*JM|*&XQB8s|+{fCb*H9Lw3L<1}_!9w`#jgk3Z}YM6>wvJPiZrVX zatG@Mi?1z048ql=gOa_;1;D0^1PWi>pOab0k-9w@F^xVi;p9Cs9IlobD0AilNc~MZ zJRfsLwQ6ShwRNW$nseE1B-m0^rX1p!VS6EmO=zn2Gy*PtaZ-{oW}%y>wU^iL*#A}# zkx#j>>sgiQSvzfc6)i*`Q$}GonjHb4%-cxtL-EbvHP9I&1>)fX2iOBv|&0 zY*@W#Aq!7Pg+nN?3UX61L%%)2pUXyydQNPWd?z9zJzxZ1!f?tOKQ!l>yK>ui*}NHK zuMQp)YW4##ska)^S@}08;<^#)T!_>OMRIho@}>@|>m32y$g02xn|cPyj-Ul`3mS@! z?5uIcJ~~U!MTspNdiaPr5% z@|F_EwQ`6(hvQ@L5Q^O&IBwFqd4MvP+p(45y#;2~ZD;luqe7LkALC_QLF|Hk4sVgj zf7zfNwe!UxIp!7qLCRyHfMd^Dj!hH(+ugN--d9LshuJ;sm6#2%;|QFK6VD-0Xu!o@ zNU8VXnO&~RWKiT%h37WaC>(RYOW?Bz;b)+4`uHsqCG7D9rn}tjbc-r>yz%X67_9Wd z#kj8UxQ_*oFO$$;DK~)&p$KoY7F{&p$0a5c_$iUnLVb_j(0Y;POxRrNztatV-7n}d z(;Mb4XuS5gAt6>S3?t}Z@W7skRqShTK!5upLQ;}JJTjQsJiqlhI{~)$mYf1V^m}8_ zMXKyaJ@0WIblV{Z9%0^3aer0-BD-O>?MadCLAo~-%}B@@m-g-Sj7H=f9U#2pgX2pG zln{3^m|=nm|ImrO^BJ!j(slrmrbT)|l@i;B^B%!U7GnH=@*D_|=X}r1C*CWIj!POh zw+##~OPDY=7-uXX`bac!PyMvZt|m3Mqr*f|7F(R%xQJuO*dShl={XzmOXm=5K<@^ zl^0r6S37^ZM?U4W)kXT4MCce)Y1GeV>5KhCuntn$wR3~0AsZEQLx5c!O zW%>woE#A$a;AYMK5|@&Tb_aTp?>Nqj>1}m;DYl>l3cWwaEj7c%pnv!RK)TAOErK7~ zK?;0XS#%{6G5%u@z3V3(653W>658YVP%N+a?Q1i{&UwG?QP$6mJO={I8Hr;x(-ZL~6(mxuI>?5Y%;%=b(ru zbZyMA6#sjuJ*< z_vy3<4-y7iT59)&_5klY9bnGM1n4g;;wy$7YiRs~s((iZRbR(jLc-u!vA+!r;zO0pET`|8 zta9XSt7%3_vC|rl%o@ObW#>Z9ukBi1c&xTdzoCX;MvrsYpX}qxg@oj@^vWeI*o}W$ zf6&{GDJ4k6hXT+7;2&{H?q&(YekyhB2`@Fejl^lNn5WwtXe#r1dh=PPcmZ^NAT)yeAN|*DqEwd&OI`xDa zmqGed(1Nup@V6X@o6E>dpl+XC23ft6QM-gB$MHPe1BlJw42l2@%l<`2KwQ#Ideh#% z0R)#u%5wnGU7D`Yev8w6W#>iVMpbqh8M72}XkdQU6tdsaz=)!9)}<;EM@$X#4$}_B zDPeMwBp?=%ViMMN)oH(LV#{7JN0qk?)#l6iU22Eqao(eVYdUqWs%aFg6B{CmCgIfM zzW^#dR*oJ2pn-WTYo8`Z_5Q~D@smM-KpK;vHatCTdYNIxOD?}Lw#iOdonRs4}dj_b+%U}KvJro?;4EYR}38>`zZt0GtXz@7ljH=95I z@X_Pf=>&R>C>?0M*>s}3ehU8fx5T>EkyE!0N_)H5S!PUmd;YxFf3yIMbS3YZMA)CsE<~fSaC`~ucOhiv0=auI1x`dB8 z{podD^#`b^*4b689*aC9e5dzjC$B!>Qj0~;WJ&*~r$p8DDKjyV5qkDZxUY5wj-K98 zMQ_?etl6=il)M~3A727)?ZiJIM?_}Ri%`y_-PdI&y2%1?W(xK@_OH{<4g12id>p$yH*ZHuTAU3LMYhFu2hXQUir6KRcg`L&<$*6*@t^|d*51kBrs%L>FMKGz ze2Dq<9`_Vc{UaE*=Jhft{&*5(Usr9uuw106z^XUTuYm;ibl?T)M84`X0aV7G#kcpM zsVe{Hor-3*>Ldy&CPB;?B}U@t!I8D4zyoKIxzTv1qPSy zUG`*sw0Dhs5!u8}M6d|IOWs?O{Sp@gb$!3pFT*@`*4~AsUn+w;Mk35>;+QM&grhsIJ2M3=WMjOO|bvUAz)!C*Z8a1S#&)({#7DWlqn$f>M%5j zf0$|dc4o^qApHdsuix}$=i9@2*6}Y3PETzm$xWnQcgFPIt zm%k4&(WE*m9?&>Ti5*w9_QX6YjuNZA{NXk}j@y)*@sN<8k+Dlp@Tvg;Y|U?J=+n16Rxj8ipKgG zN+x_@!bofn+y**u8($ccu(58Os;8QKJgm|Ee>u8b&Pz{HcDcPBJo3HV*=qNV+Ki=g ze=waT9`AXdS#F9&5UHovyTqf@`s4oo?8uJGZb`2k(*8j_@dIXWrQ*$3)jup=XkL@6 zh^&Oqc-3ACb?#dPHRZk-@-453TAsZtX|c%v-3N9h&qj;w^&TW88jBGTUewM)d!#@1 zVB7pdgM8DDFuVhFD}LW0g`0;V-_^~>azFlckP_7MwOfy1fg%;!`~mF~nE&JxAR2f= z10gNPS0q=P1BzX2zpm<2|KNDcgS-dUSQrEF8q>1Oh{2PG&iq#Q>7BYW?qQEU`t;ld zClPMO<*?~jV)o5rmHqoUe7?z4fRzk0K92G@J|jMkIm~yd&4ZPf;U^m}J)5dDek}Oc zcu|`gF-W#g*=b7a7FE|c`=1{qif3+TOHIxzzAs(RHkhjh;_YyQ?cAk&vzQs<9rFHI z*=4@qd8W4RJucUPyArXCu0eD6&jA762r(|%HHj4w8^m0?Hazj0sMJ*WIO_=Nn@uDF z&(s|DOW||9GZStm$gMnxI`O^o5M)jcyy!+8$JCh-F9XGFj2KHT(JWB&YCv-!e_^*A z#f=B9QcO^_oKd@kw!ywo9FfU9hRt)pvik?=aE4}4H#`f<+Lp|_agRMAh5KCgc&kfe zuQ|uy)&59+q4$TbwEp15W#feZlbhEa1hmdt{D!=Z+lIj08Y_rO`94PuqIem{#{;!%s zqN0$h{9fAIlOnr^)Mk$;1yxT18RAn6z7NXbPznd-q~#Vy-q~@*+bbK9g<0LlV1P@c zUT$~Z;1YB40&d9&&DHbVzg{H&_xjK5i|wmH^&X_PlgqTm@(kPY^b*zwtIkhx3O!JC z;#dn}2n$$QV;ajzGTCo#TDQuj$`i}+gP%iHu4!NlY*|)P!~@Zo^yh-ENU63LQ>+}6 zu1#bv=-+|V#jP!gPQE`+Tf6o0G*Gzp+WQbmN9F5w1nB zn)IDflW*C;4bps(f<`XI>D&ddF}c-R*)Yj-y>c}qk2$(Ltf~|;tI${pZKFA3843tU zrUh-J{Zl{`qEwUHWG;vv&&5=69%q>BeObi%qR=Bx{wy<(jj=#0O&orr3VNl-z6hm* zoZFZP58W(x+|C~VM6e+y-ku;^EZ-@P=t~!E$I$RaN_K?ZL8VC~$prz(D#i8UZ+B5| z-ev|fm%%jOZSL%lob!MNkr9IAs3p_&Y2qDcSMjbDJWgDxA0~j~HVTxj8tKq@vst<0 zHlrk!GM*b|a2V;cSWAEkl&7!>6P zxM$$mNYsEvw!e@ubo&V@Tu$%Q=V*AY_o(?uOKT^eNTQXK$F6l4-4;aLfYkrU2oT(7 zCyeb;3(+`(q}bF?n_Z9Jci^YNsU<}~{OvG63Ta|_n!O}Xu1Fda8lTd@+6(muWIufM zeiivkFOJ|}hmvi&|G7O0l>B#9hgcKuJ6Dq$3ELK=)ES=N-5zNOT9?1Palx+J|v8&e71wD_$G`Mm^xB*EG$1)P zL;t4dmjAi1y%K&>Lj=J`&H1FaGKtF9AD`o@YdOin2<@4^t^^>sPDI*lP_?1l*=U2P z9>I|RmnsE<23@ta(`R=1X7-rHKl?Es2l5UE1uTnb^-lr9T;7=m?ng%UY>r5uDKJa09t;xV&!6mh#4KKH! zf0=S2s=O+0cPYTx)exs=-ZmD*r$>UpEBYLiZwSX3tW%<3CD`bra`<3?!7u3K58!>{ zEQ+rg;a)4pQg8cAJ}1>!ZnumD<@bud0v$Hmjr3YSXO}Q&1}llb$9vUpkHi1g(zqWJ z*}b7TMGEBz?f^jD$+3QY}E;JAD)PYJvifB z*nj^zh!JXZqSE8r!RIKNB4NJKr>2u1wo1ppaGeN+EiSq);^A+&J^oi%KK-fP zp=5_RJ!W`I&?)&ZFID@gb3W)3&l1B4fP+Gz z>z_?``8?-$G_X?&sPW>m#41t|ck_{7Q1ydIs8$LYY#3hnC_wonVPfg18?9cXn^$3V z;z%H?t*E~q%O{ig1X1ii9Ebr>HJat z%X(+Lokw6q1&>Q*%?ALts7lK%v)=a_zbF(UQsn8?eU5t^l`V>@TP8QL5lMi8Xv{cR ziYU+>T#TdzUUG_gX#}cs#=qh7ox(*mZkPkKd?eg4p|+k$fYv4RVP(UkJOfW`%@dT`wvZ6hu- zraUsOt4Wh^>mY>%Ut0g(*7BlB8BtJ2Tt<&>tN8jR_A$Gg{wuOYLRylH;%O5VepDK^!i*18OR z?A3qCzIPC}4Wcz~bbH?LZcYh{b{1g-1Qcbeo07oS%f+|hqi%f5mZJ9>1AsLmsQLmw zh|k%OY`JqF0pgMafWH|R&jN}Pg_H5htBrlnI|I2ty6G4qgLTC>!b`?SN-gB4cm7L* zR-9Ufar~FCt88_6xN?Y$YdIT{7-Ca$mVKK7RB}09qk<WO|5OA&&e?#v zm{}GkobnphKPyB-=`pg-QFcLCFQV!{o{OqXE?;rj(0pJPCSiX>G_=vG-?Y3QK@H63 zC05xywj$edzwm@yoLuOa+%CnJ?2SP{d&4rqyT*{K9YD|4DU(=EY$?s8I6UQz6W zCEnNTaGL|jr*Z5Q)0TUTSB{2>}>9nT42!3#ln%Voe`8x*A`u5pFK@!CeR z&+N$C=J|aYQ0rA_0#0TPL)3`$QXZ_^9G=+>(hFG)kvsU1Z(#6sAU&iP3Dq{Q1yT96SY-;(>?TCp)HyiCsv=&UsvZXU!z+syJnUn!}O8f77=CyW<|K ztOpk8M=147XX-TJ$s$fg@MG7L1yUX9;fO+AZP~gWJD#_X>r}mz;`G04A}b}JUq%h7 ztsR+zs)?VuU(JB!>GmCudF$uc_9NXFIyV?^ zQzEJ6p1nlHjpGuraYu=g@0!OCaF3;b>WY2cp|gLO_fz6r7Y1}=1e^yE zNPijCHd5vzKUM_)h>slN58;Z(Z9k^x0oCic=^ z#yY5B`Qku2&}9Oz6p8{Wvhu)p|HH9F?L1!Y6mgx?{>NSMIA007To%dtmEZzb3T=C`%EN(eo4)IM7811_lPLve)jn6dfw~bc&c4L4YxZkWc}2;w~w0byWbcQTXX8y z$jidOa9oj4Wfca1F1)iauxN%AbGUfIkGf4CyL%A^b=y}8r|(a?vdX3{{pfxI_sXpy zEsY7MdQ4dfOHs2}ULP2ms3lA5I6fz_O~vW_PW3BnHCAuhx%qa7dy{HTd0wWXCf+ZJ zn%VFP3HT|i0n{`QS=gvAxKUvqw3Pe5aY5(s+)>at!x#P(5oAH^$$bwzMCZT+1P3dA z06>ZN6-89p;}_)xbl9g1ej2pA;re5>=J^NwCT;o83Wum0(wW0yTFpPsn+c!tmD@4t z$lQj}xZB*DNEI~|dREL<337bMD**E5=)yYX!Aie@m6qME^yw8lb#g-K%vnrOMA!Wd zY9p}@+}ZT&Wsyk>D}8w3)9_6M`!HaeG-U&_vWV&x|2m4!Lx0Md=e)Of7-cO%rj^~nz{DS%h<1Y0gk&y(FlE5WQu6y7ih%EY9 zL8gK>L>3N80FX{Zq)Fs|E<4mFHXJt=w;d7>FqdK_SS0HB9IjipVeyY?HqqZyUd^T+ znY&-6CA@pO%5<9K_HVojaC^`tP(@4o%Zv@Gcw9WGzo7g+?ozMlJ&Q@We92ac^7{l) zjnM2kYelrqn>SCF`X?K2)K0I_IxR64eDJ-i8Lu%2xXe%a2cp#$|0)0@CTbr}`@N?* zEtntT-R6K>4a&Ed)fhv&U7?n=RAeIMk4wamm53o7H(l=mE7ZF7V7`TQe>SiNs>y&0 z6#w9LdvW(g8e!4lEZ|5hR-3YFP63QRZ)@PS2rGj8J4QOUBjR2E+Qj?32|DXOQmC`> zqEHTfO)^b-B{}iTr)o-hEsyVHyn-F8`JI{nB9(Gkil z<5Y81H1<4vr-NJ{W)`>E@-=`mR~UMsQxg+k_r9I&ISUSk zJay^FaA)cG2h(j&Eb!e6f-0~-+H!IG=lV5b2~e6ZAZO{t zg1r(cpZ>cv#7MF_0Kxuh6mn4AWDd2Zc;EkV0^oXWpPa_ z2Qnx}8~sE4yx<;}jATKdaTA&;Gg zH()LmOB_JiOx{4mm;OrRN*N|4-}6|Z{*{%`5Gs?16W)~WY3^U|m~~G7XQEH{8ohG! zg5rjb_w#G5KSC$tXcgOWN6s6nBKT5v=Q5Up=Yq$lfzJ~G#^Q0w1td!A=cqFvz`k_y z0r(z09t3`I{`=wv%(@kHx`7hhJTw2oRA+Xhbo#qAYno<_kB9K*&);Ku;ffXhts!wel4fS6Dt%!rY#`I2RATOb#3A#qk+Ow?QfYOCq znD*k*(hnGNxDk5ke&I>sOP3Hg>`hfDv#_fwpQ7Yw$=ei*$;PiEMr}MESn{|{^&Mrn z0`QFkD~0#c4=5q{(2Awqd~X`ctY0j-yvTahow_%+^fY)7AUD&<0&i8^kxd zCI;H)5O5#BcX9p~m?|T4xP{Zua?6TL)ohs2Xr6i`H9%zJur~z`lj%ufg(Ymr#f`>! z=QJ7zZavWYSf78B)X3Nu*$ykb$V$sU$E=)t^R*xakD37Qq9maA${BP^OvCsA&g1hM z1~Rpms@#ZiWjCV~4QMFaW^jB&%iEMXRgMeIg;-{#ejEzhfTk|L?19{%?_DOui%060 zN#-(}w2ZO0jbr2UQIx=nMxQRK(W47W|bN3zXv=L7(WXN|z+Gf&S8h4t^iz zkph4%GAUZPN`DaOOc|5e)V)G+Xs`tTKUB3u;fj6#yrQM2ovts!o4G_zjo^_B3I=F% zRq2{sp>)Kboc9=(L6InFVvuoE+=4EUk$F&p4rBN6@F#x*n&OQ4?1>JDlbI@AyNLaF zn*)5l>6Ul4^~w{xIIMme{5Y8qMf}h%k-`+H{eOWrpoBl$6iV67pc`OtHcXb?9PXGu z3JT@)JbR4%`{Z3^eB7!g;p#-h8p{qyL&r=45Aw?#83_9Q0Y?b>q&D6GIo|eL;>)7C z?IfDN&Ka~x1Tti9f06aG!2|nIU*o{dHNHijP&!S1{2I0dx+eg23ruxw3F+UlGg5#2 z{3i6vQg}Iq8g0+pWjy?c+P@M~f95KQ&|HWFab_9`L9`bZa)%ms1W)RDZbvEp*C6E0 zH~Bt`;?&@fG|4?J|DxuZe$C!yBy@yvta?Ka+6%Lf0N)k*Bq?G*_ks>+erHE%fA#!@ z10xSU95@XO+r(OIE`WtRkPGHGh$Ml{5lncAPl#u)CmP zT>k#LUz?`|x~|9UMp%pV$vw^Ozd7_E&LoyXbs&hL_(#o>2L^k~Gv;8lr&ILs%BI>B zsG%zt4ba(TnU`AvbzOz7>?UWKMGRme{DZ#l|2C3}yjK65aEX-tvLo(*mvkUk9lUOY z^H_jQ=9ALVp`9Wd+>zWxdHN2;sldNn7GU;&TR=$bU#YG8R2?=Mb)Wg5_a+%ov$aX2 zh)GZvC+K~e8vnbxaqv=1*#GJ=2}3W61U)9Ez53w2*1F;jD>b3?RR}3 zIa+dhn!C$=(i;&t|Fv2Mgs@;VoIdCUSH-XHb}W1;zN3as^_r*-jf?E zk;0eucV)=B=?N3u!HhxlBJV0S>mD6{fzBk#tdZDtzp=;uwdf+E3gjj#UHaF{D~!ug zA4rsDnAn~g%rBdm&Yo;ZVWr;Y3rvxzdQ$WpKe?(~n2(xKjiaT$8MY3)$ikj7z{%x7 z$qgi;DuEc_?UBE#9-Mj+4zQ^R*Dsy-{L^9stxc&myDYQZBBLUHKA6N=s78HEZbq4aI~ z0R~lZkjYKLJ|zZxaD#ikrnYx$H70Sci!TGsSfx$nSG!e7Jkv({X8z6&9?8o5LP(Hjt<%hvrdI5p{;XjIk{ z&fRiWLI&xMq4hnF+K8otH;OURW^Z+PXK?zZKQ;WHmIMk(v5V`SGK%WEh|3gA5S;Z3 zyt(Cz-V2b*ZEJ9@04rq%(2koI0W(3GLN6%yDRtAgJ7CsFS$>^!7$;rWwf)5F0VVnB zB6(j&7tS2K;U4?Q0*cH_ktsE%9aKs3cIVUt{b6rU7QRuC-ln=y9{rxCrjHP1iiEJO zjUj4%OL1+Q6gL1E5YVAG=p@jThcy6Hi4=+|2Nw#q^p0H_0nY<{1F#BUowQ;NM8ZJH z4VZ$$wxMxd5RPa~*fOhQ%Fk6WTMpLdVP`6AtW-P>aJi)^az{%`m$qF%9bAZ$Jc@ZGe^5 z0WeEFkm)auD%s-WS<`~pJuI{%o(iU{^rjgg# z4VUr)h8X^Tsr&=}!KbXb?4dc0#9-{GJXDHxA>>?!xA6=rEC3SuZVhmtWgth5# zopB15!5o0vU7nmF^2s7NQgFdAOBArdBL?eoI_Ky~?&*OAHS5M*$uAwN2Zv9Pa@f9~ zowx0Jyi4slJFKsN6*TPbP%<%xYKjot+t_Y(#&<>o{IXECKGAaOzyc&Mud+h@Pc!Ao zMGac*o+d!G9w$P35$y#764UoUN6DgV49250g@|S_;_f;4h}ovd?^f8#ptcCGibT)6 z930bG)c&Q;%l_w1q68=H2WD2Vvqjr#5Q4hjrn=JXl!;fJ!QG|pfLV0l9?|{6RQl6a< zhfIhqSw95Nze1pBN>9>qRowM*l_yPM6n7=Mcmt|iVr4d%jr8oXx~-LV`{V|ehux45 z_JH8-5DMx~ZOB~5r{GN`tCd>wTDCUbt&2v?_W8nfx(c~Rd?3{t3Q$5w%|lZ!o91mM zTTqd)74uQO$Wz^$tbTJ?6GfKF%6l{6$Hj6oQn^z=1K(3yTOkhTJ1|d1dr8o`0stbg z`F=fT4r=sHZ8{O-{nfk=N)#-<+lc^>tYmUyC+jcH7(W^C!8|MUi)i`d=i}Z%pnq)Y z%67W-AYT9&t$$slO#gfR^_0zTmAXru&$x5nJBhVR$hH`%h*1|kiC16rZMlN}NPU$a zE`HCr-X#ys*>PY6lj?kjPbdt{(zS3sejywQ&=KV*Hq~U;mBIkT^UXuu#yINYops=) zXQc$Z(t3sU&2KyE@TDPZVB$dwc#m9wX_EqJu{ZFhCSUOCj=mi^JZulii}YzFuR;Q$MilGC%#^_BKF$#p?Ag# z?Juy)oD&N{&Z{>T<1CsxDjKkwgX?n&niUV9uJ%m&(^gA#gBzok%L;xfV?UZT6n5s7oIq1E&3tx zC@Y%yakAb2pEjNcmb?o>1_FG}qNsNHUQ~0HL!PerOy89q1VV6e_aF6L-E20X#%N8t zuAF(_eT1U&a>*dFojh62>HW&rxL-xAHFW%_li%$(Q~4Jj4P1#s`iOkO<81CDWKCDrMZ=3Hua5?o7cuQFKG0@${ z^Jw?a)%ENR*?H*`$&^Q3%oe@*JbW34qnU;zYx@c7jQ@dqCzvgMy^yFu>6%~ zA=nZ5tC#PM6~4Y;ObhjCkIu<)Wv^AS6j?DWm%FlQFr9C>kH@soVc%GC804!W^l|Xe zm}~3Ip`|lR;;tADvKWr4WLqV}sU&TFi;e%84@0YR-bQ^@N`PwI=hl7s*SP8=+PLGc z*t8+S?2;*o?}Pce;V*P9sDg8asM@!S2G8=JJy6|D3hHKI--o?vlzSQU(DG_RKwif) zzW{}pK%wJvP_7hJk5k_ zXueMh9*D&Z)!$$&qBh1zitct4MeH3>jRg?I@lr{qaM((wM8Shgj~L;7$m%dT9p5;4 zB_TpICa33vXWbQ>{88mi$I zJ(1Uq+`YWu{Ds6CpeC*;BX}}#i8I`dvtC7N_*mvYbHQ4kWfZh{gV~67aF5uS#oR-% z*FY%t4il@*%V0jkx!~%e2(cI|einG&ePtMjk3jDzo6pDQO5!%9$x0uTUAS55d$@P#Ce6)p2j1#&5t3&V@IRjm!*U zLaB*524?msu0J=kpjS}aEXvSLn_dR=O!~hLT1UbukH8UZNuGsNqKz^-oJ9LWt!bxhYXKLVGsC+ZiopO>mR- z;XPfae6bKGRbbGVoul7N14X(2?Uk+Ao{F}HQ2rXpa$xvgiDp92O)Neq^&b2L`*Zei z48b$^mb3W<(@D^_5b@umA)4Jb!A+}qcjPta@I6ZOc0;D$!q8jGgZZAf5>4U(eMHz0v>>PPLXk1wRQ22^*YF_EN>t_kmwe0D z`fo@u)nSf*P`lS_C2>t&B~Z(k^PP5}w<|-Qw6p2cvM@Qx1r{Il#Jn2Qp2?ukgzckc zA3AtWdd6 zQ{eG$hWTq`w`bUQEvK$TpdcAQQ1>Zvx_E+{rZ6x%j2r!E>+1y5-R+L8(owF&N@tRG zWi$OfBS)7=X=Di8fI9a?4QH;~C(kRMp_J=I)-5af2<$Or&yT* znOgYY6Cz`sXbREM@kM5lJ1< zaLjwXDn1xlyVTT+H#nwIKcMl!QDFRr$L2jNTKnX1?mwT5+{|9REETTqPkDN|3jvR` zW<7jm$cEURekDnAW$9$}9H>k?Qt5;cfdZW`VsjGCF1Z|!j9rpWnwPDa{f1PqH+ROT z$|6 z%Jz}_oJ3mg;=&_-@7W^6)B`V*N~QZ3v=Vgbjm1 z3kPzC`~iJYEY<~?4`Yi?T^Kwc#;OW5W;a=8%aSX(VJ)F63}X-LGB@tNCNg;xnsffX ze~XMq6c*h*gMH0b`d8wI!5_h40PTF zWgM>RtJ#jDs@!0nN$|0`)^)aRVdno%U!Rvjt}dNBo0L4nYwk!#>QpEQF7`mC)ij}% z(*drvi|q}q@o{GH@WNZ{ zB@PmDmy?Yo1A6I1Q!riXOJ!lxO|OcFuT*qo&n00|tp)aDp98X__TLe+Bx!#Ab9%Du zL`LDUEh-+R2Kbi1e=z|^CU5HU$ujDn=c%MAXFlJ_Tx_OV5Z8jrpH82PwMKo)i+&6Ey-0vR%s6FSZO-mjxOAn;IeId*e##G=VHZ=13B;i?iB#>aV|-gB5f zGFI-ZtGs&Vi%;w#S(1F78xu2$a`!i>r~l-urq5Cry59yL)(t{>z3VLE8{pB%8UHK? zR)ppY9lNL3dYb6{ES1t_e4{LT&%B-5S=tA^G+#@n z-@h)CAwdse80$+{eVRm5M@#Xr@7M@Vvx?^9lvbur|;%w!$oyU`m%%lpEI;>TbjD-zD2Km z=A)HzWIJZV222!H(|<|(Mvz`n{rc&0V1zxktyBC57{Go< zpD#ysQ%T4Q?K<;>3N4ChK(Vk2zg=bReM73bAXajDe4Ncxv<8VFCU+_$V&y*mF7gL4 z^FCTaXvu?ExK3UuO$2KE&0cCWuo^GNubxv%E_|Sq))&Ki%t;b?#!+Vw56`dYJ8VSP0nbMM=rS;0>~kJ0 zJ(~|M!D2PBJ{L{}6j51xtB8CX6{`_6PXo(8X2Qu5OBw{QMesPl@k zHk!<8SnR-(fU^3#$3I@v`A$DiXB1IZ*Y(XcX1Cm?S1vwQ@~2buC=b>dvz!TiD-=sa z1YWnJRN#Q;$QxacF=15TBH0UQ*+R8pT&8SVHtacbl$R9WHUEmOO@iAS1wYMlDMi*}A7vDdGRjwO zA1zEx|Ink)tapBA9Sy#aZPl`8H7ngK&Xm>B9I3$YHJa~l41#mGo}q9#2%~iyG$s#b ze{L=3`e*vJ#b3NeA`AGX%IrDyg|1sp<_oucM~u6-&2`# zL{_W9-s*2}u=d*2Wmp$lE}*~jA0O0*Sx05Yze@)u_)(j%&ZpvIy1dpPS!~8S(u5VY z%!Bu3qGHD^?q7fOp}1?{LYS!M^>PYV(QP*6(5Id$Mk{Wo4#a4E+WYjOco!{ii%*5{ zo6Lh=_{(6O*4HjVVIF9T)vV}{#f&qztB?N)E6bfrQ&{dg#7xa0vTVUKG50S` zdwhdFb+Mq8qx+5y5=Gy~!cCAtKeZKWW%U z(3Q1dtRNw8_eZ2ipb+zW@>8UvJc%GGsS*vXV6J>KwtGQ&qh{6(a^!no7%?# zn<)V{b9{w0K{}Y18F}BQAzYPuLh}7HHu&Qvl!G-{Z$G8h(b%bUxL&?yB_wYRd~JAO zFK1{JjfpiZkf4r0;T7l1#eL+*s-EEw2DJU6&=B<>v5v9}UWxI^_eZWyvBQH$ccO;) z!I#uVDezDt!*W3=uBA!C+N@aE7Itd;^X-dd#|UQB6&c z66^QvU4COVuNRi|oJF?|Yk+9JXGF`IHXQWC{N1t234g%?4!xV#JCKxW0}Fm$TfRv-~M+fTA1n<&^uasRGyU~zn7zQhI1(Lp%_*Wg>Q6MQ@XE3-bb$Lf3 z=ojaiuC@0AEBT||MG7%Ch27#6#Pr95)AjEZ6{qHX!9;a^IjGWzH%FY(wuNnRgGOSHCs{xn|=&HvgQ{h)^H4jZ8blhZ6l zm&R+1dR$1^dtPDI#C@?B%x9O0wZkLj*jYv`m#f|2WD*mqFQ0Q+x+d6=s678#%5`69 z^FxLUCn$`Nkhv_qpg$ES6Kao@K(4xe-#_uPCaL@Fgj;3Flx~GfrXaoB-E$s6|BT#%woL}S%$-#`P1K0&bK<~Nrv$a zF^Aj!lr68d-xE#gMn21Q(X0m~3TWDx;M>?rD0!rx8zc>8zgxKIpw6b0z7|+FdTjkh zDp_1S?CY05S#7mGAINqSTX>aX_<{xU}H z7-#`iR{4zb@Com|mhKQ7$wU^6pwL!3eYlXihT9a@aByLCA!wUOo6XCE5Muy*Xl{?o z4UZ@}@TX#kkztSMCDqMCT01}0<4-0fh8yBdQY@9oCR_A;8&$gnsmqTk*^?o$f~;xq zOi0D-Niu(ggL{0eXh+U435nbnZ(9FJSHkZ9$JAGbRT(vFOLwOrDcv9?-JQawySr1m zySr16E@|9!NjFGKN_TJg9^Z4`bAA8$!Jf6&JTrI9{6JSUzbE07P~D0O8{&(i#XI|L zE?!f9W0D&e{NI!c3U4YR2AD}bsLA=DZ=v=!pJIyOs7*iN%E2&jw1_i(?zvli%zrL^ z^T(IO!&HNnnVum_*-CXkajRI~(J)>uH0!mA)(?>D39BFQ!LGJkHApqHs)Uwp5dqC+ z4Nkktf2!9dTtwMkp+Wn`Ni_PMk*x1!*?JtkD`ru!fMvT=Q>dfNg}=VVui#*!C@Kee zEGkBLACECurnTlc2)DBpfEO@bA&%mhW*a@3KxmkT<{!cWME!;Zi77k2ODs$ARt;f( zJF=|>%2T^J?f8~dOBRh?qO zM;Ks*6AG!ssPnptVO;5hRuRMSpkxJ8sEl;C|Cl6;7}_l|G6^leHPq>4v6|)c7hpSc zkeAR>UM^*1Hw$Egpk}>5|UWYh&t^WPs zPRvxMho;N!p2igELczHJL;YS#|KR7I@G@GD$$fDxgpGz(?fO>F=#pxLd)}d=`~^YR z_`juQF>(X%vC1ZdPBSnAyJeIN1gAhNA%P23$#TIEVrObVlRt%g*gS~v+}g3*>u-nS z_S%sk#xWUAl^;k)T}AIPj7_<(CZ-hju5n=q2R6k|bH?+?FV?U-)OI-D9my4nVG2zrAF9YJNvQE8osKAdb;+vkzG>l?)uSpJ z?&ru$I3${Qn$zVkPPv^Ezx0Pqs*d%WF#I7CVZf&w-cCcy&hA^=>a<$9t*|kS234GQ zko|pt{)19tF7PXk7DpKnAG{G{=s~chDEuJDhHrc|H2+XYOo`j=@8^Bv3mL|qJpErN z4ZU01g|l$k^Kp}AVg0@9D0nd5PHKAMqG2dfc(r6K;|EV7bE)ew&5`BPjsA8$S78q5 zZYIW^4!+N#71791YmqZ@aKnU@Cm4VIP9Vn+rey^+s)hPj9eO6PKseP6nF~!(ps0Qk zUZASOD2>R+=120qqmfOUmlgD1GsI-OkcoBm-%#r0I|TlrM4RaIoTrulVj$Zht~m$P zLBXx=0q^7MyNr;>M)X*J-}%@6&M?siVgjVK@7-h@>3(kUIzb8w^qFKr;lJ-X$6R9f z0s|fh=iQ*=))Um9zf>c4mYJoefBNaDvNy<3@735w z@PneM8D*Sxcd(XTL_bRpC2i3VFnXn&dtdmbC4+tdn`yKYHz+ZNATCRJ z$eC}#1cNi7Mkv(kXVsP)zV=Z03bH#BO-+;=U|$HpL)S#hZaoc6R;c}|XR&F9=RVRmME8`{;Q}-%?XzR+ zZ7{_OmHTOt2+~RM2-Dy<`Mu9Q96|I<5lVWy$nw(mmd&ig(TOHJJaqXkA%k-#J^WpA z)&4kK$NFgaYDE<>0Vdw820~d((Zor5ICSsuO-!yHXZthV79*q8O_t)1Xnk_DIn zSN4ON9Ms%d2Z&Sz_TXb^@4V;gAC38>Yec!r%Id&h7I)U@_MbL^jk7#v{9K0iIPb^VlZo(t4_@vSM7_ygQAhkUvW9+v#S5_DL#73pkWkXkh`0u;o!5jWpy>4RU7 zvI?}nQIsIOeS7%-rLE44wd%u=NaW^02*MKx%}j2-J)N{Fi~E$ies9 zUK0j37wirl-x*2C!sE5dnI#A>Y?9v8QcT@5u-wz9h$=XZ>%yeyn6UembRgca=N5~^ z@+-EU``hFw;f5ey|0s&{4f;1sdEELr1F_|@R~|6gmT3ZQoZ5Zv&s@3(!kZZv6#6hR3sHxNIOlD zk{qY&++zBOa1#uVwNE4yeU{AARi@q0DvN7YCWAm4Y__!JKZ2;?cmzDAD6xR47RIoX zI*sC^9COb!dU5McUi`mIMezE*gqe@kT2}dec1Q%BIQSg~FsERF;DNC?{eS35KkOhm z16LY@R85he-T4vjNDzjc*BvW6Ij|LC^sh)vwCDQ1=|(o>lP|rrZCKUu_{khnYS=R# z9C`T}3h0iBWkaMy`S*3t{+QKp1hv1B0kl z7^DuzyE?GwCSr^)p`!$eS%Twlf#ZXwheBFd&I!@O?S!yn-GMf^**>FFXK86B2ljpi zrU0|>+6V}Y))U#GWmgj!9>!}to0Gcoaw-ZkE z(v7%d5{LGrOYz|QixE4XE}{=7!tCBb}mG^AUkCxxhyCP`!BX673 zjb#%}T2<)s9jI}X>F1j_fgdL@Ixr8(Y`d9@{?M)Z5RuTkZ?n4uCGm=w97bCJVEATV zdnGwpw$gC2V0&j=vCHU5*!amx(6n#V^IqudRF_FmpZ;2DU#U^QU z{mP1~2<6|;V>luxn;03&{%Jm9JI0dTvZl+l53gM~tf$0{dE zv|)G|in8TNN>t>L$K5aZl2~y$Ef>Fn^_*nCYOCWKsp?mhnOtD_Au7tI_eOHZYZ(#B zarE&dMZQ!>h3&Z{nk*60<#W_~Ws`Vil;`~Y03u{6(+U>g^lMKpPvWWYdWV8@aJXiM z$)(t73OGIH@i0kwRZIynG6}8jz(W6E z;l8KO12+bAS{o+TTvRX~kM#e;iouGm^~`y^Yiw$vL%ui}WLMj$OAnW!#koj8+xZ>g zxO4UFcmdn{GJv=*h46R})N4++{jFG;z}pG=h8UBEf`c@Z7#$42PiooqzBtB;>+JngJG7ph*CTHn@fim*%66WG0MX}-5C_spX{Sb;N z6$=0Ek0g2ut4vtE202kOYrk+W{VxsXjKcift&R)F+o^AnE>ca+y2@#&bTyR>TfKe0 z+#C}L55r^FxoeRF!lp9CiZwj%U0a@JuCvUT#m7Qqgr0>v%czrX$eD$6!1|F(Qb~@; z1CBoaPN$N_X%>T5v+8+`mfLIug5`#?s!v@Sg4NVyps%epThv$>c{!BTIx(2%Jg@!r zNUX?3$D|3ofyRfdf@7=Km!k zgUL~c&g^%Pzn0)_Au0Cw%ISH0%EP*w`tv*D{NgBRl@TsoT$Z{W59>Lb+Q<8Yn)a9C z`j|c7z)wvqzKkJxYvBb={0&{-3o5P_tQ(JUk%`1rw8=UV0pfS0} zZQj9D(eoZJx;H zDAFQpjx(#FgQy7`4`a^ziHOQk+C;VLOn7RMr`?PPsSq>2i7#nrReR*~8u83~)_Y0! zGtk(v@ci~Xl~xR4EiXm-Sb@BJ%tE$W^o`B^o1Htw`@AYX){DQkb;V`YJesiBL&h3m zdpbZS<5hPRp#C@qAOv_ycqgw+tw?Z`7qT^%UtS^vX5nv zv;i_(X|S!&x5RN+{*bV~Y=#77%WNE58S>)$3oz`lsATFl6qm1(x@y~_1pkGIF+Isw z*R0kaoyGCC6zr$;yc&b=PasgMoMfF(SdMyK4zN%S(|Hr%%+qNxz5r{;SG-OS*@J|% z`4>5cK^};1%W3Gu-k%i9H?$ahaVFWGleBp^Z|{g&1-{^2Gjmy}Y1P8fd&J0d4b8b34oMeGstyJXs#6M# zl39dv!QNw#h_4zniZjyuSDEnBy)HKqD~h1|WdX7KRqV6(_C9<-8A7ifsr0;S zC6k4n4P)0Z>i>O+%q!0Qapf3nGNJXb-DIYnI~Ctwo zlu~&yAq$8~nc96DFO?sNj;@(={u0B)1;6!SQ&f>r2BIID&DPNI|)ve}~FO zpu*0UjRp5t=3`hzF3(E_B}#MRmd>+xW8mF>A0u+KEutjmWAP_A4eY;O(E(EquXBK3 zuiY$ydJa?NU9}Q16FMU^&%dIJ5D~!MH}1U^{BFc~&mj}JL`St@R2EEM2L@60+$iIF z=CxiC=G~X|xp;`+O`{+x>iTbJV&G`~B*rj}0Q4r@WUBWYWf}(hm}s!%zc@A@a3rwK zI;37-<$YLok$uiP1%GuIYT9IWxID~n8v8$pdu4lyH)bxG3IakC;@~Cs4K?@@c2>KT z6fP5>6>fEmus+BFop`L>>H$KBE_F2lU|dHCl{S##%^KMUNjDZ@%5un>a8VTvdeRYI z-GM)PX(MhmE!t<2nhGF`+LDl*&|EO@;=4P3r?cc`L4(xy^0*RSka)1z{4a9TJh@^n zm-`~(MA4+9$5tAg2S4DM$(cEk;HJBpkVN0jP?oyx*g zMqxOSP`f~wUA#Wp$&Jt0)}?P_E3|VR#xsYYwv4DPN5JV(;G$)r_oz{I`9F9-{QQrT z!IvH%V3YKcb6cUgDH3$fcX5-rfadXVe-3;}$`8MuGr7xA&|44E--hO3__ma5gjEyk zOft05Pu;67XeIr1n~16p-pSjw5fgcLLwleMsNX$OA@(pwyz_9MvttgawYLQ*C4e}m z;PiX{+KVy$W9M%pspG~WpXN)$BNFAh3tkE_@>o?}&qCWoVyMMuvFP<#6~W6JC<#@u zoyFn9|3sC`eNWQg-nb>x1V62rB!XP~1E3c=3e=Zw_*H&}5RhT02|}(;?%C{a=Q*br776v$-8-Mo{8*=@ae+uX90 z5m9kn6R$fw5O0SQn3`U}q*dYAbBbHJY<@n-6j=&ligbugx}{P{CEj6%E$jUHL#Q=; zc<-I2rjl>y#hSabwN?GiU1 zjGEIKHK|G+Jczq0bMAMb%;?Vo9K+QBg4doNS#lBmkMp!9Z_t!rbSLRmZFw_8?$2@w`KpN*fG>F06c(AL}THg@f2O(Lpd)# z9Refkot|%pw$FTGr?@(QKsUVKAtwGox5n{i^Tg0 zsyFLit{QREHxm5WXAaV1`Xp8>OGuigXmAMs-h92{{eOfrn)T4R_N7ZBd}gu;O&WM! zDVt^+LRB6a#5JoPtj|q9&<%c)*Oq)QR2i%lhfdCZ*QwHROt#;qZaGuGYM-ddd>oZ% z3bW_Cee}y-*{JNC&cbI?3{czP-a_QinuxQ>K1V4=_^+s6Gk5~9dEbAfHI3~Ik+KcC zU&lWvfAVt_BY@p7%BMs7D}kAvT*(Xfa9R%r^_O>}0h$azMU;6QK&;3w?C|?njtf-_ zTdg^}xNWee{=lygeA(jSaT) z{oDMxQ}{5vn3X(k;kgm1zK7;VFa1ey=o+k!zCAuC&#psO2NCf4ucXw}6%6xw4ln&t zyFEx=@5>lG*tNO(ZmLRNoSiSby)n27cSaq4kerFF_D(p>XD^9o{KNGZ`&ysh-+*7G zFQl$D1Dh0|rDEVSzJIKC?kC~>>F&u*ISG|B7pM4)E}DAQMTd`1b^`hFfe|@Y<)meq zv&{^RFErVT+RbeLKC7&+3TcSNaPYCw;Uu*#0@co#yGRL9TfA5cz%XAjz? ze?-srre5vwfwl!z`I@wLky;L+3RnVlGQ68cFav&zDbx1;0_>}m--+@Q7IrEAb1;y) zh3<&(MOD~_$aDI0W$5@FiR60OGXtM&pq=aflG?NtJbW7_Ew#M9o~?lm zwuHRQgsA!cV4kw;+fO)`IDqKR)VEvDylemZAaxw@O&Gtp^yO%9v-}rnFx|(@e8^QBi_E$0{4+KY>T*i|}lFs{n{iXU7Mz2q%WfuglZ*AOS4P$m` zEqq)z!?CJ2)$pAJk(E7jamWFiVv#JN7Z}~wj@xAKB{_&Bk8AVV4%%4@k1s?YBBY&m zHNnZ&`1VfCP--Xf`ERu2cim{3e)k>O(ix0Q+XM6d|KwoN1@hFh|0@SePSRdpOLMdy zeEJ3eDLY%2ZZD3Bi19g6NTm7gzo1d#*7uv#88jI{P+;ul(+I5Gwm>VRwyh1&z!Rd9 zKdh{Wx>?C>)yG!r2^Kb>tBs^W`6iKq5UebiwP7x zDR9tKkxnA`HspPy&m7eeL>ZAV@GIy+*l-fH<9A9$_X{y?uj>g{Hk@|wWd@|%2|HdS zXMzTHZ%0seekMFbtu5;Mvw`GtK^{KH`Q(vk&T@B6@$QOJu@j5Z_g~=;3mUvLi z>-_A3UL+L{M@xjoR3$AHc=r5vYcm-}^nE6cfh&ORM$jzPBqOhx$z;exmaXu<$EMx@ z(a=t|WX@o@okQSLgz$6oX77CSF($!O=ZzcWNKZa})bXbJs9)w1@BGyB48# zgyy_4M&gNg$2&bt*@(sJbeF%Z$7L}Wpw)h~kUn4!O})VU=|zH-Qr_os9R=fD_takC6Ngn~}CWObcc=<8cFP~JA7C6o_(o=l}CmfH? zfC|Qlj-?=wa5|5bD&lIs&?OQ$yFHf@wR?Ooo$QMdM+n0a|8FB_UJBi7Tf$-7&LW)6 zV8D7Yj@D)S5}fv-;ZTq?a>2OFVtn@KX*J->CF3ABJ|G^Z2Lw>G3P`mT^mE_KvaoY+ z{}j2Z|NU+T_h4go0y8C=SR?Q_uP5@xK*_VbSqq3-C3V6T|H=D!rug}0WsDhEW#gFU z#rlq-bU#9#uBmb9CpNMs7`8SbLwBrzKfoE`GK`P~vdj4u?A&Nmg|G6 zEE>pOIXc_cHbDV)nZmSKaUznHS=Wqrkt1b_oLnW* zUtmx&k(i90bXW_P8oJ-4?Sy=J(0!j$oEE-Ldm9O#7+@UnuV$Q*G<;BK(EYDO5vf`* zTnR$5A0f0Wh%95Q{dL~$!|OKCBr|ev-s_e{mTx`$OZc#W?|g%2yd3u2Kh^PM_H*Tf zdLy&|f!3#!OB7*=VErXY^z6Q~8a>&;eXQ)W?E>4hk&|Gj7{l&xPF%s{C537u2$F^{ zE%9F<&9riJu#i?~_h>rWeF?>e8$i0;KT@jxe zju%^c)KY5zR@E2`N0GgtfS)_N6S`j)0>TJh6NIJSF4Yd-oXlew7SH?a7D=boCH8Bt z!#gDY8pV}rmuToKstI7h^`>owocFh$BB`;M9dAql5O|PDWuzJ2RyA2@ma(95GRlrh zD(ss8Z7eOHaHN|r6wpdAc(}t9>DIAIYAv2lIsQ)>Ys<;b{ku*WV7C7xY+nfR6CKC4 zY9)hTZ|efqFG{fSg34^#u4cWvo^*wKmaiex*t^1psA3r~hmi7^L!WYl#-%<+)PXp= z?tEH7VcxiTyT(65is~$#{eJQt))lYCyf7@004%qEFqJ6la#_TBFr5Ok_1spnt8iwC zG5>7-aT}i#*5#16z|2;%8`ye^2wwL}hS1BLBtn3pEE#(9BB81;P6(zoR{NdSOwo^r zG!R2MC)7GRUc^s^-zaukzWm#aONF#^ftAGupqYFH=ovaOd=wLWlvxjcXbnc_3S6*=Wn@0F2 z(YL?KjAnHTqkiU5)uVLvlNp4m|NRqJSwfVJ+RNz)yJhX3OO4fRH6O@O{oTcx4A4Bd zThCylj&|mVRtPyhu(RD`k%G-5TbB=esAm#_-$?)n?V=544msN2y(euO3;}@Xq(#Vd zV%N7Of~pVo$U@80z1@wWy*jx4z@zz{V~!eI7VYseeCA#}sg~P5J877f6iws&huE>` zARr$g`Ip&>HA5b@JPn6dn9sMT$pnJe3o5Ha|9R?E^Xh4enlufip24g{904s^;{x}s zydZFrgM{(N&Y$&&e0PiAKRcdDgP=m#q2?5oO+EafM-vf6G24nasV(i2Gt*RZ6eB?M z4W!%XO-N6^u0%gou}qP$T7hJirNwZ1Dw9{9o2XO}yh%SFye+$;yB5V=9B9_cLkm}DCh|hb20y@s%DsT=D7Ikz;ZAIoWe|7~8 zTLlPpQ0;H#DSvdhB1R^7rq0a0Z`C7lD#PR#Zzgo^9e# zQKioCQ(ED3Pi^Nxgs{ZHdbj$et1RP~Ve0>7;H98!^@ka}I-bg~ z*VpI@;Iq&gy!oPq{bxi}dS*x|Dwx6K#!YzRHG}~3Cq8f{bSQs9uzW9kSa)6wKlJ;( zJtCbvEQ7lsneu%@+;Oy(iSsVAFu~-;eSZI^1>lZdsj9n}fc=AZs$3*aFG0M_u-~Z8bngez4TZVzI`!zqT@>Im z!YZ?|UL2_eHx1}m9bpfq24zY@feip%uCe>POW5FA&p#oKg1KTL>Lms^A$xxv&;E1j zpv4T9GD|@7$iK3@uG6yj6n6}G94+>Zg)6kCHX7_VZPnP1DG6=H6Xk24rAGv+kyYjx zgdVP!F8)}8rDcOTLP3NdAh!j7BBuOOcm!EA?QRFhw6FmhdQXu&0_4n${wCyw}bLKcG;pc%Y^Q=#Rai3 zyd{n%e(e!`?ziB4w-EW0j$EQ;2W zf%znh#g2BN!;yw{Ou^sfn?p?yOfHM~fj`LVO=MDfCWA`(Bs+Xa9eLs%5wiv_brdbi zpA!O_=ok>iS0xbqig^w(QSa8}93iha{C3vo$Yj`va`?vPHXgCTa4v6L$t<7zCWOa8 zVvHt5)il_n-x!*bb1HyEwU46)u}y7E?b{| z*wbptpr1k+q|l$}p)~s`v4NZ1)BGVBn3%q8#9M`OFVVQ03Qt@^nrk5=MlQj9vW>BO8c+&4&r0n+vLq4y4*|DE(3%-aju zD<fwt&gZY$$H}P#;4c<*N|s$3Z^fG-99`4`S?%hzHeRcL5^? zFtfo5?4WlRYJmsx;!7|xp=E{E41h_7!4;br)s}6ta=5DoLE%%m{i)7OZgtG*k|E+r z_OmNl6?-Xk&mQjO+9%SDn4)DB$spPGo4Gl!Ys~;uE(zj^w8+}SOkY>TpLK?zo|Z5+ z{6yTo_)CTzkt4TZa(YU|Vk=jV`rkKCN({+F?Z~TT4-cap`<5VDGlE~Tc*}Y##WCF! zXsEz5c34pEX?`Xx3lshZwhNa=HPIHYPYat7f^S|b`{zjs6If*Ni7VX}#AhhhSShkl zN=R!uRv#;27h#B9?W|De&bwM`U!fU$xgYH-k(D{3_&{N5xZ5tBMYcq4+ToSfYkgKV z_rG5CG`T{{EZAILq2Z@;r73ZPnif$!{q!a8*o={$T4d*suz3|v0#zhK6Y|4O7{Om-IDyr{Jo6p3qGg6cfW zZ@a;9KpL97wdg%wbT*d|<8(NY!Olvx~LTQm>p)v;{^EO(!TZQ9G}C=qyj6jsGj_tkczx8?IXBFThjIi>OVjn z@5tvrDT|7w#@2M#|MTfJ^o|2o1L`BzEIgQ1GfNGj#_y&S%;a$jrrX~TC z??R1UnuwO8p>FVU*TYx!fp3X=)98=P=@R2ds#ut+aGcuii^Fx*Gq`XK4bG(p2gWru zdkat%TCtnBfP~ZNysYa!*X$dpyIAlWG&|QNam>fyX^9-K4R-CjMT%Vq(>CzI{iLSWqlV@Du6F;9jwmfV#MswSa*?)&-qHgqlx7}sjcW;*Vhs0D@7P=I4#cYm~8>xfR>;&9^%>v?!T-||IaFIpRK3$B-; zAomtg@s^x4J8-xar&!Pb^>Pp1mQD!hs(zd@pSpWiSXC}yA)<5{24SWCs@s$yvZ4O) z+x8&m75w%6tcfyrx=i9DOue?G!>9QflkIf50`I3sd+fHV5a85EH{t{8)E;%(c>@? z7H+8+TbZy_JzWA5SZ{gDZslQh=uu(Su-CFqCWF$3nT|Qr!#cin6x>IAh1?fc$0)@5 z9QQ7kR?@4O58ZoVjkXgi692tP4MMsOM!@y&&UZ?t%K-iEx z&R5o4gynwsw&k<-G-fgziqo0;dJ}?Nj0E-5G>jTW?(dQz>hh4VkHFWuCZ=I&;42uY ziT!dZ4{S^$13iCHhpX-|+JaPBwb$X)hG!=$dOw65Bj)DR95vI*$uC)Y^t6_wY2=Y} zzgz6`I-0$<+U#`JKy5_i)Nwb^pSD${F0G_VO^Qg{v|Ao8a17u_u_aaHF#Sqh>q;m7@cbY`vEtCNs8xk4Mqv zOvLnKjG@%3rEpl;FlkV_j0jqo_n*+3!ZPzO%bdn&VJk8N%IArI7^OPpcoD{Ix zl+|^?>TB}cyr`oCK1F!VV8a)zY!3lNxeKP?cQNZRD?#^L_8a2t&F~hQfdtX(bI5Pg z2r6@`z=JYs@L})~lMLm;E+Bit1gJFIBOB7P%*{@1aSm!DUzTpOAW4NCf_(LQcX@MA zx(0SC9UIi}>x;YI>zvvG7!@Sx!VzD!$&Vo)fi}^(D*q>p_u-CfydbaQ?i;O~%#x+& z7r<-E9oe83Fa=U1wjNA`n3*aD+cNWQ`dVTvBJ@z0S4X-)ZgLcncnQ)IXF2M$DzeFW zKZVVV9W<~3b4VT#p;Jr+tTE9h#TCC9HLOtRYHTE0NGDW5f9VKhR;oE6Cay#5tTiHj z;AXT_jB|244?k~+n0eh}E!yq{roTXA_hL$D!>gz9kWa$dH$aPPfaa-FtmLV;JvzOo zf=Vl0#y$#0lnqyRi0s-J35QSlhmPjkIz1B^*E#ne(QO_D6DyLIG|5OpzF(fw0iIp_ zPe1F#K`DU1Q&=0oVelk|Q1p262)v4x_2Lkf4flT%)0Jsh0)AQEuG8<-(SO~?-Kb;P;)Glx2DWqd=%Gy8eLU>QP-SR=lsDwop@ z;Kh$jZu~p9I`d=hg+LxybstL0N-p4E%an7q*Lmqr&0tJNbGh5+qWinT3$;IE(xmYm z`OQG~TVVzATUI5V3@x^dM4t@p&Ubr*$1W-+ZXTt>9-7Xfln)l7E_`jEBp+R#>;edE zhyUNP@0|yjA5Kp%3{SSBaE(b??QAT%$`SyWYqsXg62>T++u-+;&-gL1|YTzQi&>(VDi3whsqNqyr7G5&_GCu)yAtE zi2BQsARoHJQhkZIxM@xP<*}gxl}wC3L?Z@0gT%^l-g(mjZ;Dj4p?xl7YgtgxsQSDiGDZ#yJM{yi0v|$4 z%J4nzuNDIYRLRY&efds^y3*GKA&f}e!lMo$wyp$3qMo>vbScuWVx!;Uq zY%OLuIZNlhH|dazi6|@;F_C^G;HM0t^})_OY>&<7*$f)n3A1Y|9$M1!uj*LdW#?Op zk#LH|O+Pj&J}93C!2uR>B@K4K&~Zgs0((wQ{eWDL)@J5Kl(b@77EOQG;L_y4e4W-h zD_f-k$_uEoK5^BR;Hc;^X@1ucszL`35Kl5x@S=%{GNo**&)k4K>z!nENZFB!I$ScH zv#!wHd++{y$A%@<{f~-l69twQLRkyr0vy+7bBjrjl{S2w`k^$-z#FFET*Z*?Hz=Kj zZVZdGTVE+IowK2NtYRhgx%6qun@l$#ZD_ZaL*5abDzP3OZa*bRUj)^bxU?i+wgoCq z@S)o+IGH6xBiz$_In@PAW0YQ8_}@9=dD??!^y;8(up61gTOYR0g^}g^q3UCv)ph#y z$fKOtqMR~sd%hG|L%;cGER1iK7Ju)eOurre!|1c)>%^#y6`cPQ9WRvl(tn4b3D)?M zmEyTIAL7mJ;rKe~nC}xhXjtpNdfQlk6IL~#((uL-Yt5sWnj5Anyz%fm!D<_6vhVNb zmxnAz6JT>pF>~Y~=yjv}pZ~}B;dx`9*vZ2B*Dy=ByUe~rj^7F*LKa& zQ-bwEe{4muYm=9#XmF>;QTx% zPg4+)t$OK&W$36C-FI<;?9GIzAk{Wr1*?Bu2HpoAw<8VJ247u5c=M0hisB?PPDf14)pf|vT;YB7Z?A!(b^TmVIn_1x+LVpN{ z9qOGCll*fqG~iNW_354GT-f`Ka~>D7-qA+V^StGS*^^@!@WxrO)jtir*_ZxN{r>AJ zXX<@TVh>O2j!jvDSTi}l-G$&Q=GpUW>*e8;f8<#mgor6emz?k9v*X=$va7zdG)I&; zH*v=Y^yOTo!{W?qMGkpH_D@4eO$V1LZ&#ZaGbnYbdyYM=x6W37srKPFChQ6Ue!8M! zobMY>88Gt;n51-8Zxaj{c|=lWVVjAH%_RMnAZ1Y%s|&q)6E#qOe+(_0j8fUwS5hRY zCu(qNQDq4$<4TS+M!NCE^@t(5y86fJ`yj;cKNngBt{aEMISN>C4~Gp(>!`LD3q2cH z$YYz`b4ya>Fkm`3xLr3dv|Z)sj%b;2+P@KUQbTj`p(3fuB*Uwkd3>S_5L~r0CvI=8 zOL?DuI&}V)j`AK#jw80`(3;a>)VJ20C#PwOz`?uaQM*IHmn2gQgl*K^h!#N7NJow@ zf5j_HPvonAZ)&l{i;o{XEkRtTAs7^zb)uqbn}2t$3)hou4(xj4(2@P8ejemgFy{74Y5qxWUetNoomKS0-wlQpb zUh8Rp{G-L)Wp?G5Jm(j-q%5vvR2F$uE{&1Pl-t>_?fmn)qkMS8z;}^Q@R|O%DxWg5 zYM+klx~r--?w>bWLv-Quo3AQG_z$-f@9K`shLHXl%kLpG3%Sm3#0Msg zvKst$I27CBInVdJ-250i5{L56kt;PY4}m^K?2fr}1`>3-{Jbd?wv7SCSUFv#C{@e2 zeLAFI6bWdLwNDRQcS}f7xRR!R-RK>$+X@YHyIR53y$GiD{qNA+uI&vkF zQ17XoGUnw|jzn;{aj%E3QqrRXp(gn!t(F+T^rf2IPPe75T*TN7?&zL#j)sV70+ z5Tq#D~Oruri;L!dnBS|exF$LO59?$ zB55Xo_1c9g{f;xNBM41JpgenoJZlrl^$$OyKt}hu!dzJ)<3laf0}buEBFb9$NF$7c z>h80UK!P(0(?-R+4s^uai-SBV{~zgaEP6>+J(MosxR-Jz%A&!tf>ja`XszqW>|ofy z#oJ*FN>LNpOvITDcT8^wE`GGobbIaPMtPgf^DCz0conCVplWk;$YfKM9}yofh62{b zouLn|f)=>-N9kCe_4!supC@E8)N{JmJy<(i#QtuY*>cbb4pmBTW%E_!xE&pNr@YRk z3bokxuiU%^+_udTomB*=AUJ~I>-|Q;diNXu@|$Kr?5h9mX3_aJSy-m*HF}VInr3(d z9r=UMS4A8jKQhO^$C7+0P;;CQvD+>}6o<9nc|XMjJa~48v%U_U7dlx~pZ1;%g;cs% z$bmVok@cFoOop11w{-B-%M3-gvPh~jc|*#B8)pPUf$J`tziG{2>#k1Y|6>zdi2@r zi6wOm7z{NoW|fVamN}Nbu9R}9D<~hwb()5Gof~_pgqrPI316N78K2Jk&a70Z%G^_} z-pQSr<^QSM;-mVw>JxOA`5Hm^^cx|CSqRk&EqyY`~+fV;<$=| zR2(B_A0>`MqcHK1(#gN1m^8JF7G>u!@^uDyono39J%7vc^AbG!L+qDdwkFN<(*B0*WI`q-Ecf}_90+}lNET04r?W25`LsMjGkssD zX1IP3eDyd?&pyi@b}FXmxm)?%%(vHWjaxoxkak>^EmQ2cJ{0q65DW2so_`l&6RF!( zcoEF|d}a8td%MP0kUdCr)V;{|+yS-gKwrhj=dpYJ%X^QS4HsT8;UOwgZ45X($Y3cf zQ7J?g8b~ePRUtM*p0?ojj9VEY)4>_Ve=Xqf-4599zIk z&YF*K_gTK~*^^t%jJ9%y)a8jQCM;Ha5)7kvCL=m-x;|^})(Se$PPR$4^#Qh@4(8(s zl1)DC?scUxB^JGP;G*<5v4Nr(39&tc!B)A+OZp&*Zx}ql z?;9DLVUDLDXL2>IhS|j5O9hv{bM=BUc2QWxaT7Yed0d@F-_z!#U}#Ufs|`(#@lGdI8{DEtVAMUL0PF@s3T5j(w$ZN)_XL`dfUJ85(_O1Lt6LAtj zRL5Tg=#>A?_KfVX&|lB$xBXN4GGcXr)n#dLL2C$o;uJj;j3b zhGzS=B3-`^%n93D5YL4CT4H}2@KhBRl>50saN1Pf!Po(l9{i5D3bYFgm38Kzz(Z7T z$B$eIhHcX_%J#Y?PT1L_1&8o&UbCFMO+VM~6g^Ye9=RxwQYXD%4!fEktGD^Y_6tWK zvn2%4;*qJ2FD=zp-xIP3@yKt`pi|20@X}n!{2}P?L5i^#;&*8QV{|}}>S--pt z@wWLlO(M0O^a`(lAPrj^T6#_U>%;cHYyO*oX9w^s0g*u@+Z*5K9{Psi!u1@~>Lw@s z78mNs<}_qd3epJ$)x}v%&W>a7whz-MJg+n+PeXb=k%vThH+|*9u$fqN>tpF-J3eL> z*~L)yK*%E7n5HcgRZ{1M_G0wKzd|CNLy0@AK}0NN5nEbsvQAW5s~B*$PbNR!MOQ zR2a3;vTLYJc7%eP1an8HaOgigcgr1@(x;+(Uq5o&deOB$kL=n!l09i?86A380+M=V zb~mo&!m2dTSggZXsG~k#L1ny%;;{mXFVDfAGMDXGZWP}BTaQ9gmjA7MEtJU)I=B)z zRjV^!9dxkFppekH!;fKL+q-Vr2a_Ov++2Ea4B?o@Rc%Yr637j%x#;hk)nfEKtBb2I zpnxj07EXT;!*_h>qU(ve0Yphc_dN&DefI&Z`RIo+^VpYh;!i#Yr&@z@frsz*e+!-m z>Idj8m(i8bF)j$GyDoot!yEXj(qkL*JRJPeS21<;7-|cP*#DvTLro^G!O>qm`nEiT zsYCyS!pSGFj)=_8Jr{qU&YyaMKNNSYVy+CG_4IlaPCtp<@LM2hnUyxQn*y@l;PrMB z001BWNkl?Tm6i$8{>-K-VwWcnl?S_cR(enj?CRp1y;dvzgg>WI>G;Ak#&wG8? zrXaM71T6lCqDZ`|m;Odz0&LrZX}Zun z!nAGZn#74m3Rq=A7>A8Ed*s-vdfP>0IyJP0V9|0xXJ{()BaK>IPAzx~yE^=|LRXwIj|(5lYo=a4a;RbY7x zYVTc`Ir0tcdgm?}eiBvNg&ulP6%7LAh9I)=DJ;~GNz0(e07>*X(XXir95;mRcu49D zfd|yA5a}dqy{CoH=2-@QAo1K1(=w1uh_Gw{uIKXZqJ+vLs`(-!3uQr)Bk>>>z(iSs z2oH{9^Y;r04)kx+k&qI6-oZc}4w4w~t{Wk?=_G-mIS(P_8?dN*iFY}$=3Q@iZdSsZ z6b(8;!A$^q6Fl`BU%e%cC38m(!w+o4z@9be+dho0k$iN4yhe~U8L~z!5=UI_TiqWAWs-p{4qu zXD?2E->e`SKw=>jA%Nj8(Z>OA`F*1r^b4bB?FL=}bSY5v4|Nl0t1t;{{^S!#_H4ZT z^~8Y8uI)(g*n)va--U&zp23;VehzM75rJ{x`!jk5;*vlRV9+qplStsCs$e<@;M+Dl z+qq({*x{D-G$x_>u*?5gq9gXBDWS+Z4Tv`SI{-I=Z1`9C809= z8iK$_qHE19d3{<=bk@_ku&UEAE0Y-5_V7v?)=h_jtq` zqG@#=G{cn9R*im6>}%6IJ`P*vuVUfr#40}s&sDLSc@v0;Tizugw5{2YwbtoU#z3?| z9_$?NKN_=u;`sAOehySs=2-x`DxqPR&@~N-RCEtd;$F4f;I>1K2i}DM*F@cuqwb(kM#E%C zYO$agY7j{1l0&3ZWHTi4_q(q8{De6+br#9+To;ny!Lbc6Q3b>#G|A$f479GffF>&N zd>M`vLWTuVlwdoN1rc>M38Dff%8^YHStR-VodC~^)}9n!dw709;Da%-m5b0??+DNB z%DKW$m5xww6JYe|<5>9a%uPG~zIU#{#&_?)x_h=Dx31@gjwO*xBC&5HM)pPC!t~}V zPb^~g@Fb?68^icxC*aK9997Kv`XuiD`3G0PNIK}CgAO{l8R42GES~x<*6sU=Tk`xh zZ-TJEi?3SctT=ToGd{6P{H?b^aJO-1&(s9v>4$WbfF8 z?C#qz@ScY;_L)D$DvK8TD=7tffrR=(nE0X z#`{;mh;BY~*3%N^PJD~c2N5qFTs>(1^6T&AYbxElgZs_5qgm-25$Ei6);~+1%1yFA ze4QV6JJA|(TI?Jl;G7EaF!B&@4-JkgZo}4CqV2~Ojr_=oydz7I$P#2Je08-FyzN2|+Q@mb z9wP5|*9*ANLIORB1Jg&o2327&>$f43>*Hd99|Zhw3AXL?ZORe@lZ4j!Zwf+9p{Pla zHuVEK_veTSZ2`&ATA+f1rOb--PU-JU_9>(8l9-8o(|MvT5R0R*Wk3Lw=1>u44N1&}Yn$-G0IIC;E+N};VAd+A zSw2!}9o^k&zQ07Ubdhb(af=ky0AB_e>r5+-gN+O}9kxc`RO`-u^cPxHQ z0h8f7CLfzBi%4a%FbtDhm{D+gDyhM!S0IEc&ny6sJ@fhdhW1OsfxzTAB%H33(oz62 zU#m+U2ImM@SGce`Va6*QbkZOH#lMF&b#qm{>l;$I5T(|vo z&_RCx0M_5Pb=A*92OTUIW?uO_Br-!t^=`Q(=a_DZEuW#_2&j7znZR$N1p!vQiblD3 z+1pBFbJ(zZKW~}86q-$vc_qZ=WB_Fqbrnp$`gh3o?|gIXiQ33>S@b?|ANn790Q$PM zsGT|uHx50%5Gb+AC2_{Kk@GyHQYlmw1+MGHRuGri9k)yJQ5-*uGtWHFAMO*`EV6?G zSAPsw2_kaItJbwD@a!s*-K#gue_`|~cus={UEZqKr{zXxJw+fda0+(r+tMknx)#u~ z!zfMCwbHuanrc>6M>aFj&z5pFS*z;1`Lm|~_&I+YhD)Ru+DY4{fDl=Eu(mSrmXCR` z^g0$-r!NT((ch2=KtwcI4G}4AxsivsWjat*1r5XF79m8;IgSsj@G62(Myl_2NMht+ z9$A4z+?y2phA&mAua0bj3!&W(s!5v*al*3@2ON|A3!Nt|kwK$64<_pH>SvM6Z;1=* zqw_>$Fp;Trel<;o7Y9%iS>M!-xah-{)<_U{k~B$wKZwTUdwv`$%i=Y{cr`k81CaF; z@vJm93Z`}4AohW0y!(Mf4f+~=A01C*g~SlVB3Bf|9F59zqwR-rF#{KnSQJP*f8ueZ zdN(7P-*8?&$5K~9M<{qbTHPu%=YI4(ttS%eC~#a71xNy&dqB!*$k^{H%?bvPr zRS}U;B@FbZA*mV^U4~-^5VaHn*FdGT2qwt<^`(jd+p_t!Ad^t}eLUHfR?1cQzRe3! z3`axYna~pno{>-~TYSBWqR#`-MIo{@BGG}?nG?ux0}Zw-!w&_pz(q2h=KBe}C`esW zRCulfqhavO6h+bbb%u*;ss@(=*O>rWmifA)YoPDD@mh?)c$Y!gy5^y|wywg%sv{Jv zaGZSN5RU%Ai#N@9^KTo#eZTt%-u|nPV9ovwP}AB97}e#2stZ!R84T}Tk6j-h=`2@0<^Ba zA^$s}Mfx&f$>X2fbO>xI95?oPCsHnmt7x0AX?~W9F)>ZzK@cu&BWw$orSDBEfFMcm zJ-~K+$Wjyz>bPWE2ySmeLfgXV_t3NHoe)G7lFZ_Y@}@|@;!c7k5#vI+466w&o&Zkf zvWP$@VaDg(1TwFjCeoh-7N+*2JoP*$JVhk~Ca0px{g{9kLXpoodB^jx*Lb^h9{rev z_kD05coG$ObvM1<4-oQwN1_4|$%x5jdTkW23@!wPygMK|L7aT`0!~Pqmd8EjMIq`4 zpp{F@Ia;eBCxDx;3tHr?z(cMbC*b>}rO00!0!C#5^|?bB+W814lM#2IG;O%st+G0J zJ)%p{Qa2J6R8yBwbS?lzqKbO~>2wbk$9@2a2?$DtcPJK$Hs%&gG-@?uGU@0wfd|L( z(Us4}7DYB@W{Ysp2U$(QC34@hF*|RfR;qBjAQ=I4J%NN8KoJ>?S{ajb3^hwcSC@oL zQb9@&k7hz}Ir2tYc9RIU}H`Ja;>XPwe-~D-{`?J?|zs2za zW?!4UuKQdm_}4uJE&#&H#%t-f2nv&~+%upBByk+%PRCdt8@*%{T@BT-04eUm` z|JsXjQ~*W~+0Cyv5)Gf>wVH@yb&-$9Z@cR)Oq{{O*)iT;d@=N`-;ALxJ1;BKfW`jz zw2!;HJ?u{VZz!NpB|M1(BnVVI@a_lGi-`_I?eSf5h(0AmQVh{8hbvN;IsJoAqi@p# z%d(ybzU`p$!pk`R@BbqfpL>B!jxYK5WNhF_5*9KU9IsYkcpf(pc%!ozKl7t+{W=`@ z$Om!X&-^4(J^5>P9G8RI{4ripuOzxLeEa`+UH82bK*X+N6w$N(-ep@)s|l-lJ&}Et z=hQK<{oN~IG&d*O^J1Hx*Yv1A_TuLuNIHggeW12il&dEyX@VxE05pgaB5%hk* ztD~vYqbaNyhKIuVi*Op#5c~!<-Ssh;mLGZ0dp`Uyge=LNOt&l>37xtHy(k=(@}Lx% z|JyLBl3Ri#2b?S>vLA7EKKF7Df>s9r4U7EbiFBo}L7b;R=aIxH5+DR28Ygudq|+L= ze6ehYZ;xziX!|5i5J4?TAXsUpjLw|?#;9jJAH@~iP zpv9kc*3U)J>^9=n^sxvOi7ioH0j&wLsJnkVa%rKa8XL&Vp%LgwYB(M zDMj1KC|JRmJ~Xl7MFQ@V|LgC-)_2@~%L)Wd^llkM?=KGGuAh4lqu)P{S3ma*3g4Q# z?&G-YQx9Etk$}*>u9yF}8u2~cSfOCU{oB#MV|Z20eFq(MaP3i^cpjneT{9Ano~NCF z!Sn$;U?@5aW&k9mkmmyoYXK+EZ$!AS%tGYk0GcSE;jwd~?Yf87Le-Nf&ridu*SKJy zrP92jhXRZHH*A5PzOv6-+O{kL8S#zla7Y|D(GYmYL&FnzKyTe=O-oWfXY{Qqh7d!B zyc%$!pid2ul|p2skar#2XcW&r2U*K5E2%503cByT3wu8EUvT#Ge~Z(9_!;<(i+6g` zT#BBD{z?T&RmEsm7p4pYzG*_dM2%E)Itydt_~vImi|Skfd*1&Dw!Q7%>sIEM?%R&? z)XT6N3uqKaSCwUqQ5@qoF_N0@hy>RNt93n5C3t1}Al4oD=@l@Vn;0z%q1f+Qh`Is# zw!asn&wLh*;usRS^|3XG5c|T1kcl*PJ^ndH#A*=R#t;GT!{;Oxbr(poz&*T)7^ceq zko$@Vv3I-Qsu-uLZu*;MbvSht(9ek^qp)F4NMk9iHmY0?qRMlhgbRUJ`0_3UCPe|E zt(09KZs3AZ=OKJ}u8%}QL!*2auuI5odK(Wi_DP^-RJra#QKD|M*YO{*rBbIVycsLO zcx5w@??l*Bhlyc0khG|?fJpYC&HHiYh0i0^)s6Y_gGlx6L8I>SNsMfOY-qX+-(|2x zkI%hhJDeaU@qnym_-Pda?(t5-0foczt^tqMkOx_!HRy0*f!1Ka1XxyNfut%FBuiwk z$k!sbrq&cttN@+NO2y{ZM@{0jRPoK^9(j^*^JOGA)(ml%Gn@^sa}^*snp;i zjg*>#XIn76J}lOKFfve?8%43;Lr>|+k`Cx#^PDXiEi}tCLk~$imLI96|zxNl_)@& zo+3y|+a4m- z6G8}sKVU3%6^N3AOm9CI2gqWkQ7Ivj?&6_lS1J%RK}R{H1n5;S?u}2ouXqf{Y64Cg z66_$Z(u~D{rlo-xSHM$SF?B=C+9DS007n`ka#DaUDa4=}Vn_>+5W^dihd|aIi=*Gi z$lf2nF8kBh3}W-Y`gus%G+zGIPeCOH=pw{FUt_+HZo@#AVIY^yV$%0f_Id*fQV=o`Xd;aae#+u!?Av@TAT?%7*H$T8D>kB8JK;q6%w1v~%x**^0`BUHK zL}A~Shi>`fy`ET&>#1=1JIIY3=vWTDX~f~Lg3tZ{s?|6pBbDWP|~E)q9X- z21R0+Ir}2AL;H~;FLB%D$LR6uaD|V-TI;Mn~7yHTA!2)}j&N_GI`<>-bzSSXgX86nu!h?bD>2KPqp5Wl|S_3g35-$tZ9vkCV-mp-`*Z zyyKIKO_C`MreScOc)oo`c?HoLQFH~1$s*(aj5{$*c#i{^F%ww$JKoT5u zcge`6i;$HxCZ_^~!5|>&5DWpmT@;L9Vs3#3HH$Em&6u#cbWK>ZVgkd+B zw}4WCp;(jvu?v-YfTZRips~ ze(wd*+7jBsjjv#?JJv%h6GpL)zqtKBUl;QEN}&$xc;FA-kM;L-!o`}XOcinP^UvVe zAH9Uo2`=wNR|+re#zYBZHykQgbo-ch2Bl|aXPD}?KbnvDz zclb+isxxoOiov3LU!TDafU}l>NlUGA0v8j-Mb8Cj(iDBkZy>z$IO?%i4(v3pHV+?3W053QkdaW zCkWh*=uAG3$w~#DVZ5Og5#n{6A6bJt9(fq||BFu`zi#9jAIXI=cl_%xtJBC1-htej zeb;55HxlKkmr$NM2rV<%v5Hv+tfuv3lqWEE?CV%hqKMF`i@X{%!(h=?HBZ&oQAK`S zof?KX{`CKY-VJX>X5e1)X%5&NYi3PGEax%|sxcv9zWuHjuT!1hPmvorM#ErURqhy9)s*g|s%9e?2cM3Jf z;T7GkFCw5!DN)2oQb#ue^oTmFMu=*~f~Lx-&;AgSARsw#0J^S05PU9vP=`!pjl^Q1 zplKJg63*btL#rab8Dt$q#5Y+$HEIsrfU58*ML>dSyYQ_VMo%5bYlYi!y3oTrN$AQ# z^Bwmd@#`6jMawAUdMO8`*>JRU6B(JWk#Io56J4{&qKek8z?!XBWJ#t(85DsB7Dl$0 zbUhR#g?APBfdJbRAuwpocH0adg={QDo~fxjIr zAv@fSxBnlH;4L4&7cc+C(>U_`FJANWxA#}??T7>&bkIQu9b7dWV-cm%r?GDT$FFJ= zRP9dIIHQJuaYNu$@kCf#epIP1H$6n#yxzSw;+w^!>uXgJ$Lj+6)e!r#9w)b#)0Rl& zT@4{bi!6{R9B*lS*AjFao;F6&k31MI+>ucA7!K7$oNkDa#n?YNSkf)dzh!TK)^2t} z__0_(!iz^%Pd-p~C7dz>KBqfUKF>5*ZcylKWAO~u-}yf;`@SxOfpn{k9bR6uSnSqu}VqVu#B7q@(P`nsOOemk7#EV~e42z@Vc<2}Z z1vcGt=dvGPe#3njf8lc|j{N|c{v8nI#Io=6M!|PYEROw<@4aWkeYfm!-bk#P^%RD@ zbE0R{+dCq`6=KPn0xhzP>*}H3vh)G34i|Z4HIZsvYwkg9_9bNcx4|@tG>g2oGg%dZ z8^E@0Zo@*vJxdC3DLI&loODklRNg%RM4bp!5nQVpP*nx#v<{nWW$XZzYGgwr2_YQM zL8b0;pKej6?fKk_hOCJU)8>{#U^0IVk-d_v@}D>AL1bmbe5jhjy~;(IA(d76G1(R{ zIpy$tAXT+cnL7-n_a1b0rMcKa-3gRk(5(E9@(qHPh|-+94#^v#%^E!t2e3qqM03UX z8sMReTmPnl>Dx0x!^^@Bm|mnL-hntcF8}3rHoCI5Sp)x@8EnB@afu zh^i+eAuwUxJ#!nCRB)@T?jJdl?rrQ)wNpA9V+Xyjv>D4zL{Z{P*F83cNtz zRr++(?UFrcNVaN1oW&x>PdqICPmb@{y*SY&CiwaHVOvrby+&#KdGp>i?zW0nMceP2 zsx5u%V`hP}FhnF&6FuE(Y{>+8uEjt1e9w-=ie~9nGy5WLS-za*)%o;Xp*a2J!z*rJ z8-8jh9{S|_AgS^S7|qHAWe2?N-#mibKYACQ`}B7(`e#S3$v8826a@Lk zwrB_b$`cJc=%9m}4|A_Q#w$s$#)^S1L(`Uk;|+m-r2t-D{Y1fT@g*|0AbuTMm5JdJ zY3qcHcO#e~&?5!7Jslt?FVD4(0+sV}$Zy}N=}*GzEBD_Fnq+>D5@K`8MdR2%qUXR* zU2q;w8v>^7=n;VX(?;+(U0K7Z~qPo&%Xp_Sr>gx({N~W!Xt(c001BWNklMY)wbcifmlGO6&7&xJ5R!F)N#*`e+0Kb@=oa4D$qL56>ZM zAq$r2A(=|Twk!mJ%YC<9UqWrshMv$l(N(E6ps7028I4zfn+E+(MYU!i@JJjGP_NkB zb_0mK?C0i8gpg1(>qsXRek^{VqE@XU-$P#aHnLeA^|}E`kWg*-T(A&h@xHNe1P&WO zZ}%G18Vv}h09j;Q*dTF$+vpJa9fWbk?*+07I%orH*`7o;R*{!{rDBoy4zGYFQAbS; z0@@(5i4d^KQ%)8mLxWDC8Kjmf9J92Kc7W6xcqjP z^R6BO78CpQI|-PFi_yTu3#ehYmc`~p0X^m!^k_Sf3R18P3z?LLhG9U{R4!VPPy+tG z3gb%mm>TYMro0|)j|gFFfK!z;38156Xu}_3m zO=Iik0Qtc|9)6r4;gIed`T4D5a%>!3xqfbYHML0D1qM801~o%K*+?RhRlt-q{6I#- zifq!zUd^(jAre&@35nFHpzvoU5>`nJq7J9%uO!qOAuKC^rZ`BY6nLJm>_d=bBs33#r1I`3>cml0F>ZtiTS4#9ppt);a>sje^px!5q7IXyS6i>tqyk&_M@p5{<%1n8i`_-}#9v-VlidGq%9J@yE=_ ze^nOZ>Yny0vCG(2d*wXY78Gp(1xrB93%FemRl+APBMA6PK>O0gkOu)<0gE2=-sCP= zTctn0p%+7})dOtl*@Btb@1b5efmH9-*5A7UFlq`oS{GU!7;p9{o3C$b$#h)QVNkFc z3J$}B3H)e}%SDoFefw5lILY%aaw9iBH%Uxr=)P+&_Wk)EZp!fSyS&(~$wWHTya7 zSz9=P?C>6_$=+q#*Np;&?AGRwLJ*Y|v7T;a-0JIzu1O1{Phrg+A6WsTxw){!7d5R< z1Re%Vwmv)vGoX-GhgbfGJfK*H*D#@|GB)pjKcd8_9U@b zYVglLT}z?XsBv5?7I3>GE)WP&1u+r3oD5vz-#YmO=b`Bb&tl6L z5?5SKVyYlVDC#P(OosUd8@6kptNV7Gc;PX~S_Vg+{SvA*6+uGAOQSXpFS<}|1^sdg z1<{09@4^K0Q0@=WAR;v>!WKh>k_eBgtEuzDkH~zwQKEHA?pD4Ra1xd^xn^$XD8ymg zLCd0r{w;a|QXzvx?}b{x*MTYsTrA?2FmhBdV=$nq1|WyaapqypG4P_|W7|3bciSSe zx`&h~LSP;Yn<1Hy;JG#rLI#WO(W1av#{YeJHQ%cX&2FbAxy`M0#3<7w6eKt9@fW>nk$a!M)J8q%iyZ1pJ17#T(iB z^Lw!8qj#@>qglN;^#45%d*;Sv6m0s?PIRxo^~iw^I_Ti~fI>pgKKohZ*WUv*)py1F zEPD*64H5|iIHABrf@@*(-)y!eYoLZt1h;@im$iKMujOqAbv;lQL&;%qL!s5)eYFqv z&DYS_({hL{NgrF20TSdpt7R~E{2M$-lWxE%5btC|z-u*;SN)5Z5DCKA%7CgemjcKx z56x}T_NhMJ^rL-Kc(i^wX8f1EqY!5g{xy2HJa|>>X}KV3D)M(9z`%RnMJ}^gc=Zs3 zFvLaE+rkj4=V8EcpeB>3Q?=F`bQUxPBC}q{%<+>*cjw_aHgdy*%W_myrACB_QJR2V zok3<`*Rsvujl%4)uOJLuZWXhV*3+$sTYWt(jD8=omPK~xjukMPn+sv9^=~PUfO~*b zCms(4CP6$B4~UpEoe&|n_%Yyx2~3ZjLU~~l-Gf^o$qe;I16o3Z(ePk+(dTV%Ul+e# zdY;2Qw-*Zr)~)Y{;cNO@HpvCp{zj|LQiBcQw}gbSHcN-A6`=vc(yx%V_Um%c6SyF+cP-XqhO+2 z$5>KAAtj=kl3*t!*dz`KGE|6Ad{T7S}m=s z4lb~yVse{AOcE1$$5UK&Z&?i(ri)TNz^NGprzQoMj);U7q9 z{P+=+>@_$$S;BBn2?GP&Se##gSD(gEj-e~Xuu!s5b)y26d>ZJ_SO^6Nf*`drFBC=P zLC=x6;y3#T;`dJV)%9Z+vwZ05$WEZWjQ)pPmRS> zaI6X^dFkJ`3hE1|P@Ox<#kG~Vo^D0ls_V%yikLt3Ee!8^KLoM!+;pXAx6cW9;B3f= zCh*Ihz=?A5@uuL;y5YihJXi(=3roC$zjw`M1oh+S%J(DJGstaysH=eNTK4BMXwDF3ikVQLEOG z)W}jOgZcReEZaq6;S4fed35*pAP5{D4qB~Lk;x=^-U3+$63?dVN)$NiN8&y`?{|3J ziyd!|9X9(AU+2`I9fBr%!`hJc~0Y%NU;tP%Kd)iV7{a z35Q>r#i3(W?Ao3~DnE?!#4E^XIrR0U(5p3))X*n z3p@yzU4rO-vuG0BL~N6fzZsBN5b|z-Ytdam+o6hn>RM=aGN3Kk`21b8hcLJQt{GTJ zV_TyKSg(iZR>E`sMokZ3?%3B5_V3554HfgYz^#F(5-|wR-Jad3WAv5hFgJPFqc95#2wm@z-37@YfG$a>G6s`;l;2>iO|fX^#3>wo^7|Orv=O#=Bj6)$5|z=Xp5sKF1Y~95dOj?RytETsXfIbAJaD)>n}k_)AW&#=!$9%m z3Ai;G##{+W*MmJ@facavnJU651Yk82XtRU$*sLvGPmMV~HHEirjL9H@N&i!2!mIS%Ge7BGF< zKu|3subL37GK!P)=n_4Y&dx)o3$m?!WFMok_x^dz`F_mz@%x z$t~i^%mSv?=qPtHSV^*#36K_5b~Ny*T`}Gf-*@HgzXpPCAgB8b(qXxMc??4mq$2HtLfu>eF@n;F)JJlbgRgvT0yRs)Kz|b7e*6`5b>$!`GA0XU)G8)&Nd?x-BG#b? zlTTIACF+o}B1(ddiUiCu8$uvpOCp6nRp;T|A)~yn0Lg@cAPk~gl(;*k-4bTCudAhC zb;diLc(+p+RnJpb0afagebwZ&i(1)(86B9=tY#&5cMc>+WXjB`h)@AJ3mcaH+UD&dt2ZI9=tT2VKX%DU^Af1%3 zZ9@i{WI=!nO-s;&A759D1arzb;9Uehw}B(kpjiMe2$x=8EDSGp$K1hY%baOTgcn9_tHCk} z1x*O?O)FKN4QK(jry^UQYw>Y7X$yG1EMkGenB%;HX0%u{YoVPo3*&!>ZU0dhL0$?m zsFG0N^J;zR0)v}5@dS?hJvf!Rv&B!JA1!Pf4V--b`&~_LNIe!; z%u`0fT{$1SQ$EjYxLS}%KtZ8bx}I(%?1e>?4<5qN|Lc>epPi&~#*2=IX10_};AOII zGEIoV<%L1hU*!h-@z5{)9NzXbABP}aORuL0^CupMQJF-he1HB!Oadjj_!4L^Bjf_E*~w&q1eN^88{no|L;hgCK8;yw^dEyzU~1N z5qdsc*MT{`0B>R*O0^DmZVG|ra|I=|p z5kt_BnYFPwNMl6mMn+;ddDO@F#3*udFV?SJi*G#k0+=LX?FJc*Vjd&I0kmWV-Q5|; z%!4jxFg9kwGz{pfiogHzF|6+!z&r2XfpV#exl#lDIpDR?lgMWWFk1-mi$8Y&fBpAg z!)?2^VEjxKdUrp*@}*ZXI3nRwzy2srpFE9(B4E>&UK~1j3U~j=PJH{@({N-3h9$rg zD>yD!aAv24lW7Bv#4sG{cxNq*N0mXSH6W4DpzGAV1MVkJb0YAt>}J6?YZa%gW}M?9 z$)al%S)kFi#dbn83>QHtpiuDPGa1KbG`w^o;6EG6Hn2HAites1B&7ho`8+JA0!;{T zY;+2Po7Y1QJ!E=@u`o4@N)6byWdIF_x($HQ)1&C?%VPWO8!>+L1ZEZ-OwFdTZRZ-e zjf2>=kg^uANvIeHQ7S-FW;9e~(mB0ZRlJ>`vqO z!bW`mP!_u8;-MX5NM^brGap+wbfGji0Yyz>alU|>#W1pYJEmvG5V$55ok5H?Hlkw5 z=uP|Bx2}TQHYvytuYqlrA;}6aE}}dQ+xB3yG_uKn+gtITf*=YMkAwo&dfq#Kjdg^A z6^4`FI)o>F;tMx?PBdy*DMQ3$yiH*7)o&`mR#;W-VAJ^Lr*J%$aP^>jrb@|J({@vef%)%b+_GXFq}(7dP<0J*A}Gq?Lzl( zFI>k#xoku}@{DmSrsxGxU}$?N=xQf*+tIX(Y5JqPUJzG=1E%d_tX{{nsRq8SRxrj) zxGX@@6EV_IF<4ZQm=)1)i0Focem##bKmHp2_5b=#{Qjp8V#E3_XtIYRuT}8O!4ls3 z;4s$Z*W!mS9m9Lxp2My?hEZHBqE=Z%zIPa}9%?`n8`!*cE&lc|Uc)OV7NIFwq}5p@ z@r2-W_*hbOEJO5)H$^gAeu~kUY%g0{jy@iNooD2jPx5Dt=5Dz>he2u!@Cz zcLK?j#51az0lh312;Qjbd$pi>oeILGYXH6Hd>^RR!-y1jd>)qm$|(=e9|Ou(1}Qzj z?K?DV%8j8gJ&)nFgE)AijJ3lFxNeAT+qYoy%wg=hV-L!OA}q&5!_7hSYk+HEu26%j zSp3=N(6KSBTek)~cW**ht`9GK|79#1gD6%l+=*@S7$vFt2f)f*qcy2C@>8g&j>f?dk8gAc2`Dh^|Mdckhei%Yl6)vRE z`;XR???<^R6yz>S43Zee!N#oBMb^H?q}99JRwjz0m+xP{Tp(ZkeII}8s+sc}1_s>r zzI_MS@|l`&xjcqs4ku;Z%*WdsasbaNa)6&Pl0Ayz-mwHON9NChpn zKO&K$PYKYggy@o^pIz#~&0sp5qp3~namVld7RLVauQC3|e}?+>^y?mk z=uD6?a z|IS(@cawd;RHT<@+ru&0^j$!{g9?A+>(i2Pu05T6T7Q@ zeZ|}(KZi8K9uJ%qsZtrP>B0~KO!Ni#PFTSxOt2t8cRj>g>>hNzBx3r9qv-0|h&4O1 zP=gvOl{)_LzkUo~_`4_ZqYn&1kUh*#HgNxa8(<5A2uvG){+IuVy$5zfN_OFMfAbm? z#lpuvxCaLh%^_eETHTFfCuZ^9M|J|rL4=Nu(K9MO_U`-edw=>DC@uP&K%Ocd$J7(l z9kUyba{_yJuEEI(39@2iW~PC4Yj$EyKQJ?=w(i+7JnS zEbe}a1VVl}pwMV1G@WC6Wo^@~8?l{q*s*Qfwr$($*tV^;V%xTD+vd)F?C1Rl^UJKO zYScJuY~z4_14SCYCns6xT4VV(X4u$iUP(TEA2)xC#ou+_z8kBkBtI=^FKkLrNjK9WH?uKPLAoAm`L{CGWKy7MSV;(K587AAt~^O~jVb{lpzvx;V7 zW=0Cz7Z5n5gUl>!Osz0bn?vH_8vphg zGiB>y#AL6ncQ`;FPPk&plGym>DR%j>9}X+cG}Axf_dZLWNz-M0abvHi9jM9v^P6N6 zMs6~C^6hFCz<|gwtf@s3>Z!l*;h~{w`URZEb4qW7jgj;^x^u$@)5*YI_R4l?5}!3j zNRWOr`G`V=IY-*kl+Mkd(>@m;OZq=AfOEA{-7?5f+oWcN7Ex1e{U5eRe0^)oB#nVh zF|d3*ntdNl1*;^5*7d=mbWY&iS}hkv$snYwmj|j1TujFHB>#GaePpnI`o!wMM~?TO ziiWFMXM^~_z5@bV(7qEqscMME_>$iBBKdiRAURKez5EAcEMJRL_g)5+82w)fuwNyfRe(5$>EC-hNP1Ts%q0nIM(z0p32l|;AKe{ZLMz`*1>lbUxN{Phdqj0Ia$gKwBUKHqnu zky@P#bKLqd=aK4FnsK0q-Xh0YWEU@@%Q_nkV!#xxR7t3G8rmveq~#};=q5_p;L*o3 z)TR|*kyu?W+m5@G9vA<-PRzve>^&R0|+>U?^(RH7sgc4?w# zPbu_O-@pf`nxQWsP8wLk^% zj7>)?X}F>jeCy9Ttz794u^{?1oeb?ydI+fxLwtBMAHpN zxhRx#U3r6UXy+@48^nX0jz7+?Tqt9aI!%uN5U5Mx6q6BZ@DIY-&(TP4WvX#n8e3|I zt!1=VFfS58RWi1}kkaOsFIBDN7KV}3~;5PeWlxdr6+2ZtVz0Od9 zravDT+@3@%C6XO`x#fzZFwF=d^PE`fBpqgZ=2#>C=wI#qG zt$vR5F4w}9TKqZ-7n}H(nh=9JNomEMy!8_wQbRiHJ5HE9C?*s?1GQ)w4V! zS^P@ct{tyjK+ER^CVnjBJi_-Lyjeys0n{_t?CqV;5wLbuV7!e^d#>3in>5a3Ef}+juv5o6>hW6NTZ^lK;~D?B zL%SQx`vHJE07YxZKC7e74f-}(>lw+`N~Qs_Q6^c7O5`&VqV-o#dHB(D0D(^ORoV#1orzEIM zCEQ39-T+c(HZ~-xqPteme!mxk({2KiH??ZH(K43ed!1f^zlC{M%ADaI8VKLKq$;Iu z7j)aAIW9gY$4Y&N(pzkb(0OdtL{?oSwZ3hDNJcsIr6vX%epOm%{6fu$l3xdj`*KkGU#KHlvXHrZtFU5uuX`v z9%L!Zn1CGHFS95q6$5t{*cceA+qIf5J{Ui1s zNEJ?R0c@D+EL`8AU=;bmOMyB)AzQV($gt}+BbV7tUi(dgQ5Vk6$O*D*EXIu(V#`Ww zwv~hlUNZP8bBIC#LZ0!AieLx9&`rp#yRZVczg%%|Z*I$!MbH`F$G#reM4uMiFQb^@ zF3$FE@Td?TLav9231)>aeYe>zI<$#4IL3~;f&?|YE2QVL$sC4mWEcjB$_zjR!>2I; zhz><3+zu!uby*Rntfl5QyOo&mTZjhDKmq!?{-6{*()|i~MWMjkUzQPyR8vw0iM9QN z4Di@+ck2nVx7!6xh%Bn1@VNRe4sms~pVUXPUEN1`o~f5C3CWEZZLUGbm$8W2_$m$z z0Gb^a_Ryh(Mp*fFZojRo8fC4A*62R{3UcsbjYg+=v(_*w&hrOjYI@2gwf6kL*45Ao zaTG1p>V13C&*Sedq;TWw9^vB&PTSSdk+GZOrs3kM7ae~6T+x0b@Bf#I91$@ZUX21V zcIGf%mg}16TpK=dWU|xUV$-ihkpm5k{1Z)C3uDNA<(@`xiDn+18BZ*iq_Xl;ZyhkJ;}2q znYyQ57I{5H98Rxb??^n;Q5IUcY9N5=Cy8@wq`i* zFf?Bwit$=cRXwov**sBVQ|(4oMOggO)vzmAbt6ngI^8F5tGui?xXV6m)}F6e8Oee& zB}Qw%Ph9xLi)ZLY_9=#G>_*`dTYFw5K#x{``6(^(@KW%@@Ah<*&tnxj5WB!P?A&7% zvV=buh+{S&eQ@(Q5%+!MaF6;vzq(GNpvuw-u$`M2Fh~>BM(FiRmnq<*!LMl_t=w}6 z25>W>gk5H{*lghKji1Qx#z7DZ6K*@xGq?E*y3CcbHgZH08`$&8g!u7p6XH%=$Gn8y zn76L(k^%gWo$TMAW3BIyB>8x9iYh%2h0z~cMQWQCE;msnQH3Z=2IYoF;0&x0Z;QZw z=FByg)CeR|Bdu^avl9;`@dcWw__+_F$<6~dp^CGJHwMjM^1z7i?wTdfiv&LSPY#BU zRQcY}jkpE89GC-Ovrf`bL?18o4atQ)HU?O`oU;Q2@#{STM$D4{v(g-uefCr@iz)FLxw(gAvvUgao82ypR1q_AN`oVLcXQk3Vl*ffDkf(Zmi&&d zXS$k#vM%&}UhmyEIamjyH~)3;X#B=6G{}M_o=O+ZB?ur7bE4qMPmf$9Us-}}E!wrW zsw8)$baSy(S$VxG?sl1eZPXYdkYbW<+HzD8ZGZTz<8tI!Z3hw5*Wp^zBj4+$!yzib zeDgfTW}Ba~(ioyXgMk|?Q$ks^ly61i(c$EtxcUmfQG&8`a&kY$U<=exGjjGyw9x;V z!YS5n8hhD8v-JlktqOrC02u%P`8TP?CDqW?YLvo=yrOqAHdKgGfr{EuV=b}x2FI@ zVC>~40bF@ibOm?iLkE=)o4+2`gG6x6gY)So9JVIIZBCH+apLS5S;OfNW}UG*N@&A& zJp~&c$WJeVbLdkyY&_rW@?K45)))Mn0^d*Az6QHh^mT0Y*{mNygYP~LzE<9ZjFRZ@Q77FgyEgQYKc_-<%GPrHf+3=kpyK8^iiJ@^Nz&v3hkN)*R3jQ=9{UgeP%x(* zb}rt)kCWoQ`~Rd?=R7#c(}lLnY@wDnIHzHuJ)Tjm6{ zWyu-~htFBAjx6o+$`UoTk=^9q?e~XdCsE8oA<_K)4WK`j$@gG0h&VN7K*0_2Lgj#j z#}x5fg3VO{tpJ25k!dE-iN`AhR_NA4m#6j@m!~E><8)gFE8OZDX3|CG0$~AlA-{j) zR)A5;N-EMUcP6Py+#=%M;TEJ4n#`TXwGeAcyfIyGYr&oK zW416mtOoxGbXl66hv2Ya5Pt`_s;-{z0jtH!1~esZJHyu%O&GSo0$0O;WaC2q5bX>j zo}Vzsw6rASAkkmhftnnOr)2zk5>BSwIo%Px4kFK+&AggU*U;_iM_6h~?iLZYg|S%^ zR#lToE2f^2k)KsDpAF7CfAfK!zgoW_g{uC2FBM!!MxzjPqN@4>`!GkIzsR&xEi!Fw ztMt4zS`T%G^ccn9dadoMjwtJ^2g5{8gUA{YgR>!H3s+KwJelRAyF@Z~W>>7Ch4l7q zri}K}ZXyhq32qFdXi=U#P{q!(BXVji6K_J1TG{N_*#%BSO?}+{$Qv9IjIi0?2Thcg zj=3$876pI3E8yy*zem^CHW}wbK0d1tg#rrPrId)%;Qj3DjA$www}A7!=fIw!+4i5@ z^8Pe;&cWgv?!NBF35?%mAB+;Jub#aE+!&9uHT3%bF~082{^*4MmNf@YtMaGa+!7N8D; zPnt7~19cYcrsN-fxoO?sf?bO-WVUAyifJNW!+67I_Qhit9rHBdR71a7tW&?cvlqoY zJO1R$=Uc9|xs|M5vt2G6qWuL#XrljQ%6k;l9J)RU)_cQ#3&rLMZ7Rr`4{MtCA!&E3 zt2EhNVp_FerttfVD*cVd@m?!4NQa*=wJbP$#NhU#M|l;#Fw}!l63R|@9!0PgUcE*& zX7j+RVZGt+JpOj;W7R}4NJGA3qH1*8AUWp zpq2tlAD}eIg#P|1kppbow^&7x%rVKKOwjDFZnQ*OU*S7O1iFV@JWc%6pK}>bPiU&6 zg?T$NDC@~DzKNvl%b)KC_G~hp14YcBUsKsscW$n#;-%co<|#3vpx$C&5F&%r(x>95 zEXSo#k1eqFdA$vPrbr5zQ8`>y$b)f=IXy16+%;~zpYeK!>E*_JeO@@r=uptn|D^5s zWEhHut#vI8nJoP$;m0lL4Z@9z+uD}kVIA;hdq%i|)_uYBM;8!F0jjR6uK%?OclsdA zvmvK6$Lnf|?{{xe1dB6b%Dv&PSysWP0FlcZvgM7Z(oJB*&ldJ~=W(+r+po zNa4>T=%}ljT2$0UXoQ(T`F1p6y;|Eq}jGyI4YBX^>z!>_LCG zJD;NBz72gPW>5b8x+q&}rfcX2PZz6R}!%;BE0+`FK% zx|bhcw0qGe1N{5zJ`GS$Uc?@j`O^?Dg5KMoPk`GLdfxbJA6BW)SvbugPM##1`tUi+ z2u1oV?+iLE4VL8Vu$LK2@vXr7{Ij>`%+cYyqF@>v2915IA7*V;CI3@DurBRNq*2&1 zvtx_~s;T)Zj(-v3U>=$*g(>%a(E)x%z^GxckKI0pN`rY}%#R66tki4&_@YxGQr4~> zGFAVjKI$)vgy%}DA|o58%O2ypt*-zuL&m&fW{Bba4x@U^J36vZyR!tv^ekU&c=9Vs zoQ5%b!hm`4y$OVDLnu)qtJEe)ZMHwBI8aOMzRVJW!zr}eAYhS}pDKc1~`K#V!RU9~mRhVjv z5bdt62{b@s(o!5_b6{WVCBO?sVy^}7K{jwI@AW7asxfmPvaR-|3!O_ksu8^sCQmuR zIW~UMi$57b#dLrlioPUG$T4DWkjTq-xM@I$>3S!LFVw@Z!F6|oe~9lnR*MdrGg3AEJ}llqsKw?P7iEx?ltgU7&Uz61+g7ZA5LeH^0N0gmxc42Y zuaDZQ98ivi;_^_x?a*c?0mZ!`FEQ}_0Q=A3)J(K(;Gzp~gvgcYpV&x2?l+{p7uIVWliXp#%c!Nj20`DR&=+*j)352UiKco?YQ$ zWn^y-L_JT=U6AA6-;Z0qiF$aRp)|z=spyH@;iEV4e(B&x67{X&n*cmngXfM$?W8qO z=AxeW`2N}8h`2D=S5Cpm$z*#r1CV!yr}fodJ=w6nw^w#|dP&WQkj`u{#R#ymGlHoO zx8xz7r3vb$ri4kFYDc$!0<9ujm4AWiLAKnTJz;g*@l>@1!?yqdOCkM5V4N!?nTnzW zq(|gmjTM3u>}Ks>aN_mKT&m#4wpC>rJ^p5nf$57 z8L;hvcsu*!!j_aOmMPhdW*j+NdaDza6cN;k|5j|8(qD7YU$gvA4G|TSEoZpPnz3Zb z_1jurqY`16>2Ld=P?}?+hVHGhbh%nY%2SKl=c4L8u}n2=sfm7pr)1L@zC2f=+r<6M zOEK>O8!{Kh`~F!t=whZLjZAjzk28Dr25RNZ1F7aB08HtW9ozO6+hn7JjSI(L4%&Am zgf3qiM23);sxUlW6~*)uj1R4M-hF)8x^q3>KCHNU0*B@F+)4PS94aZ>6-c}4E9l%3 zLG#B}mJ~<#6RBv#T*C2=McwWpcs-qegbqT}0z+LS#TbwbI|Ckwt@-8h^1v!5Vf@JD z^c8|BTaZb>_=Sn%+2z!y-mevf<;<;ZyUfGe0LSz1{KITCSr*xfIEyS#iIHr9>hpOU z_H(q8cO1w=TXkvM)RrJT<{;X4k!-)(bAi;N^(SY`(DgPsJUQhJEGk_S{Y&-DZD%rLa~m2&3b(E8^Wt{MBMQUUms0?8CUJjlP*Y9t}bLOU}-Sua}&y zk*lvw>vM~z>c;y1-dnH;O7h3^lObZxIFKwwM(A@Kwr)mRo%}9A165xX#cTVWSuxzYn?4w!5DcvVIxv4i#U85r zNcZ57qz64ej}r1YaI(51;6VQPwj4Gb9Zgewh&o6!*6w7G1<=@&d>R}vGI86F`*3>d zS63U~vlC@!&3C*!=Kxw(-%ZnXeJ1&IUJv+0<3Zklce$=N(s^9IkgpjFUTe z(tR>*&w-u#Y63U-$kmBsMx`~9-HK>`z+`NbRRQu-c@7cLzAY=%zpt|jpt&DCSw=ss zyx|heuL|t0(a-9gi?;iJ~t4}hzV=b$*ltZrtXq)RT?}{ZFzh4St~vcyP9w` zC+v}P&5DlZmYOJ^UDy!aAg|k87S1J}#PJ*@LdF*uu{li>9$mmZa0stEqM4OD6EfiZ z)mo>rjevKH>!DACf|o8S+WE_Q;WCSZp~*3O=tBz%`Y#T=}mR z>42Aqq2yC5w1UXcUO0SPgP+jl5Y*q>(>T?8Gi!A6rsHqzfSfY-|Nd+IJvNr6`RTiy zRv*7)V0iS#2B`CM%OM-1`aE0ahizgy{3=x?j?-WBa>iPhuhu)Bye~BRsTD|%nb^=4 zef8J!C}S+qcQXr%b%|!(pA+1t&p`b9{Pyn!NIW4IDm$G&0ahj;IDSyX1j#G-r1He!ddtG;HRsuxbId3@N+L_Z|Jq?Xx9n-u$g_on&UqrKc_ zA7gn&f^aUECxNt@AxgGFft7f3-HvYWl4K1+jhSpw<*wXlX{DtuLTG&d$>+$iJ|w{F zVd+W;4LCkg`K@>{ak`MSaQQ^<=;OVC?O|`=6jqUn)wX4=lp)dNbT9XVAXa6pYeawD zrW`%*`Mz=Z-sZ^MO?0*N_wTDuM$$Ow8wI;*{`>U9@TaOXo%&nV&N1RqDN>ZW^Fo@! zW>=lnSjm<$xMUv&ySkcRT5ABl=wZZq2V{nN0i%&8RV|mJLK7ou4~>WSCF|eK=MQ91 zs0f~(?w&W>V&v<@C*Z&a`M{+}O1HLEs$#WM%fWl}I(-}nb5mx15+Q-9L~&iIkEqeB zptbB!|2Y_J6LjC(UU(f?GZ|hGEz8QT5H{V0C^`+@ze^C^nj_?zTJ&Ii+KODKQc#My z58$xPO4m0g99%vie%1IG_JiV4O++SD2EdYj9bl!*fWpgivr72F3-x18wKwwyr zN5W>AMZFugvAB^Mx2lx%{LE7_uda-;+Ch;@p$<^`NMdAI&|UO`5J8SO5qs_FL@-Hp zQz|a3Xywe0uZlmjIx+uD?tq2Lq9Wb-6c{6y+r4Ul+qWiul}D(g$UZ2Ny)&l!CV1@> zzm{sKH)YeC>`|IoqBeaqA|paGSrZ#}jhFrC_*BIet}7+4!6V*IJ-N{o0>L5KjQz)< z1u5?sp>FQardQpl0HAl(BYH6vd_*2Ur%F!Mi3Huk>o6i`fEFB)+sEg1o#N9hbS>Di zkad>pqZb8M`Cv!XwT7oDu&g(STc^i~0BbK3WZJ28Vw}TdekX9*6K1A9z?J z8(hiQB;mCutp88+;LYk1m8B@@@3mz^bpM27QZ=d&4b!)t0$mo?U@3tD^Ytk&fHU)d zgU?MrO4(8sj{LNPCnvY-`uc|4e>H~|PPQ;);cn}5SOsP`xi&Y~0KG%`?t-Sp73iH( zOP)-t`x-Q8X@)uCr;X>~75_Ut{oj)Qs`lNI!JW}cNqIC6|B+wSuARpPdtJ3P@4b(U zbTWW1-gwPJT$LAF9pUV}1{L%Q9N0Ep7%#=-N zKba6iB0z*BLWRo4!&wV7L|-or!d(b);iRzG8X&M~`2wkF>ACxm5SSny-v$;}uD#wy zwY)o+iZh&!{kjfoiRj2|bN}E}p`VqrIGCMpJ!RMkyAk!h;wAJfP-2*C1eGtf-jaS> z%}_l;Rx8S_m?Ds7E@f514CgBh%@1Q!wLSd$T+T!H+2$L41(AsZh zandbd!Zr9Cu!%Qb?VjYc`x$gg(vBM=dD9#Y{~h%VzI#et9Q~;(CXIUu{j`xNi8*sf&=Kr=?|;6j16R_k@LxPWtKV zczgd6SHf7`2)9rvUBFy-4tZ#1&d>V`EjflV&%xh?CEF0Lhf0(2M~NKVayf{{W31gx1FN2R}7Z43Yb%Xo~oe{+AAIKmdN{O$z1v zb($m<6_edC4HRtL+vR;M#8S&up10QQRE;q5vUKttcHG$q(# zdI8m7*DDNN2Pg+rb-&INEL;L6LPM3^Gd->2iY!vP8Ej~ zQt?YOiKW_j5Ygg>NL2uK?Cgl^Bpui)$Lo}rJK~f;Ras}PC>_p!7Mh>_+^6=|6#f;U_>*Uz#+jivy zeuCky44kiH^0QBL_J!DUW@TpC=1~`JN4>i3j?jNTw){N_gCQj);_wt;`~9120Lk)% z;X9L2s@AeOYXffnM{Tr1x+?dj%Ia59hD~g%E4x>5aTq{hzbfDcD#2Eb58zqqjll zE3LXtnXz_mDU{34*N$_i?}}0{80L#8CX*(#C5lndK;B?OQr}V)1^CPS;BTFQbh&&U zx(P?$s00eY*E-qvVpYT9x25}*xrJ3xHMOsOu7e4jE;ntb<~;Sso`0l33<+@f$#V4d zQ1rVKlxu$?#$6h+8qsUog$mhnntf~~`xooMuO@U{e8fN0ugV?&gXN-^`s zXwc%bbF(u4Yz;-Sc~Ip78aGpO(ly}EDamMS}b5D4N!d>_Iek7 z`Iq4j=A*Q7n6cCGJ?Zg2EV%nT?@F$_f7?t(SS-Sa8ELa~Z2(+vw;cJ~B-pe_(4=Jx zIOjN4`0HpxCz0FsL1m@a4^_N9U)P*5wXHfL4kT=@DtWQ2sm)3Uy>8iyu_8=rMSwT; zY-Y$&9|0R)>#)Dbb7NHFRX0hx9Vy)4F4b&x!G=)Z!DfqU8XEZ;Kt@K@p`&QRP<{ttpjruj5=AILh9jwWAv?D; zL$fD0gFm;HYHfimstfQ+L=61!~UUv%5?o??_KvYptS)Snzoh_|v*DxT&)4QEz z0JnujfQ7wz-pUe4;VAp|vjkim5B%}DW4#ivIVOWeFK~2LcmexfpG>Y9e$!LGB7m0I z+^sP;V~TDuqeL_-fBlUXuK)U z1m$5)AEax|HM8PJmWI)#M9|jDjfg>d__@mM-KzSxWoSb=;^mH3R27Fi=|o>o#qx!g z3y_^6>hGUt9)6Xb99?VZANV7S9w3GAysZgAPw_v(c;hHz`}5bt7AMQh%%$T~nJzEQ z zxm!(R7Q*KGFo-mZnB2Ge25Eo#Tu1R1G&2ae`eO0=^bPcj{KI*JtEmS#dNc-@@!@wd z@w-NhhpnI?%`kA!YBo0*x66RLFUV>;=B4VOObF7=i%=OUP$rEe!x)u>DmrXGve}Oj z(74}W6HB&o9|e=!Ajvg?Oqd~oE+JPk%hTO+hGYowBTKhKqitpC?=>7)vM}EmTHkNB z)V<99w@vP%YoEbg9P;@`I)4*ajzNUDo-ZzeIAZauxVq&h%U02I2c5?xP4*`N#Goc` zwdUt{mY`w%uM|U>Z`bo6u0ct_W`(o-o_miRz_SC3%Crems|w1v0$?K z^nM(O)kZLhj^ME~K}8fzXFct4P5vow@aSEqjwb6UJ5=}DK49Hg)tb;0DJ&2{m6Ar0 zwqh@0GgX~qad2cKnaJt-M?;XX1fyfd?K9A4$U~hwU1a^D*dPd-5-J*A4o&ISokXTx zoG6WGb;a-8yKRh<@h-k2+Q56r_hiVGd&fjgcInsR3pw`_)P-l~ee~Vj#1GfpsNPX^ zK#*~-^_wkV&zDrUWnsmp7PWZ{wSvDvVXL((nk*G0KnJ@vY>B~07IkNrji2&ZRnNX!reKN!O%6VGsUm+lQ-mlJ)bIm?rw@`1m$X$_WWRgw2gD(4bTcms?y? z6GYJ8_G5tHNM=fL9?gCf&YwT3#R;`(fB$@h{Sijk?K(pHeDjY5l(C9==kloH>PT%jgEqI8ytB+f|jImbLF%AnkZeqI?yGe0Jw?evLT+RW5*_n=RNhyKqRHG-l^v72tc<#og=sd3Q8Ssa$+C`0`M-IeH_AsN-W39RR+5 z{}_abqhn%{5*qmH9BXqz_kD;pRTIHZk|%rs^|IYE)Ry~Gakm1o-Xr=h1$*DZhGWfp z-O-gb$|8JrHex;c71t!?L z1e>xu9O1M-4C>w`O*P%b_!#2paux47j?bN{lgu9|;Wo_qe_3z2sQ$lOu zPS|~(gJfyTzt{5T0g!GJhmGgTC#T|&;2n+^3Hvqjh4S&4Cd2^-prM@fAmBWvan-g7 z5Tz(s`v88ecni&n6Nx|KjZ}CzE}sFF5yLJpi!Ly2%imsFQZ37Ty(e2d3#4yJ=7vVC zqj`808hXjfqW5k0e#`aF=^yidb%+Zh-^jJYv{>g^Oa*2wxzt@b*DT27WbGsDJIA_Q zCb}bJal5a;@;O)g?^iWl)9|k`v=PE@6ymuEWpE1`SRjjNwH86U`WC%aJ*ao^YkWz_ z(+UqPxwZQO9!cC~8(I`XL^G_>712t3dUnNB6Ye`na0|9qxk@!+Y+G;o@AEp&7WzLQt_&e_3`t<)AyjbI`l<-o$Bo53i-S6%sNA7%e_rFPHR?g3btwpTcXg{ zb8Ol@In!5m!E50ZwpBRPwk1?dNh~Z=Aejph_++TSJ$uh%z7{9R6!_I<@?&QZCd%%x zpkTs%V<)i^o~&c2KXsdv>d;MA>Z*cj;^Y_anEk1#F$|h;2)L&c{%VWapNvC&WNp+x zJk-*wnp!zPW>RAN=4Nol;EBj_ws4WmBBq#&4-;u}3{%2kCLwZobbtsFn~DO+|F#E?)46XJgu+v72Op)|0ZaOL^WFft2>GmB|dC|r9P3{2iyiI^_;5E zCfPTF$gS(N2`)j&H-Kc5`x)&cFvHN(Y8j(#o(7^W)eO))j%xoM9IHjJi|0$?r z4JKI}G5R0_O<>i{I2hvnK4Gbf?sn%De&f!1B+6iAgaXE~p4p}(>e6oqMRAkt(i$&g za=27Rj-(jDYyjE3x6CiWoRCDEbikg~-ok%XJ$mnu1Ri~PMEiNlu=Rdb5*D;Ur18=2 zZUL23l~ELA&(sh(wMU3#SCE3U|3HBvd$`^+Fs<{q9(JFZ*WO zU0rqOsfyQ&G~acGl)Hi-(Ns9vbBuN9zG|-`>u3xpHd6ot57ape3@vm9%?s5v)pMC^ z5Dk}Z-DTAF zY~Gdc6@}Z4Zi?mgUZg43UMbgX6jb@r>yT;7mx~r6o407+VyOQVDO+4l%_Lb*CfZz^ z7@Uu}5%Gj1d0d}%e9>u6Tb$bhFTYb{W)(F!5@c=WfA#D`wjyl1CTW&5_^Hc&oq&3+S1aB}bDk^_6G7Y~bv()0cw>}nS zpTTw8GWy=yUdB5@)Y2wzSI;cl?EwG!$`Qieu3IVBJ)dOacQ}$Hf-6~aw8btr7DS3n zW^o_(m?G{Q!V6ge&+jxY@Zy2%B-|YYPhQ12MJLKqLN511kZul2Sbt{IPzi1zQfO@H zx`_5%f-wGciCMOXq*KUISIjLU_Uys##d?=h*-kHQ33448vvW)18^1(=+b>8$G;qjh zH+^)X=3w=l_3zm$RxMg(WNCEhqjLy{=p_s!H@!y*A*HHn+d`;bGFD;N!OqnGj-+nyLm7dwO@=4s!Wl9=*5FZM{v( zE_>^dPJOxg>3&3ogn6P0)lB9UaIf2YVl{2d_RjQWC9-VKYu1Gxg4yeW``(tMlmh<1 zC>r|6@2})oT{mrY4Bvb30*fF-9~v9-RN6T+e+} z1^3Y*KM!KNWm=}-q^X>H9PHi6;6#>eS=yQqr;fe{Ew^OERV!lK{!cPTz=54y8J2`m zCHP;&ly4<+K%_OHF%*r6?1EZOvOV~%9!c|8+?u!x$x1^(hMg0*O-hdifM0MVPO9Gi zvI0~03rKG;0&=v;HFdxa?|4Vyx{*JZ6sXqE-x_OtIII3^kg0Y$HxF7&Bz-_+D~Ae?HGD zGRI6Ry0+_fa)o18pGw&<4S2j@byt&B=6SB+x2<1Gm5jv9MnT7E=8LRl}S|NOfmLr_B%e}17_Dw=(A$?K^J2akW_e)PbElCS9s zmy$ivo=2XM)dCF{-VNN}Q(FOjf#zmZy05q+8}NjlgjcVei_8AACZ+Z|=#^HLyk)pw zK-RP5hB&HpCpW1_P^Z?7LDZOiU6OAM$m3DDeMg2!bvxxNa0%g?Ae`RB|0V;^GfI4- zA*v-{lbM5b9i|b!aSyXD(oIWt4cZuusyY^)uydqy0ih+os^!z13{=)Rk5}W}(HHZj zKDLcKtS3I+AB&zN_#;dd5U$4~ z(F(QxxQRXN3}GF~y=f(Ye**DJKL$3*YF9!-;<#&$N5UH16C;Dg?JOg@+f6?i;Iz23 zlqz$~@w#yhgOhz^$>)t9dlG}TL(gv4DBEF^qL}4*2RVgr7UPfle(y7swm7&=$@u;~ z5w7dMB&n#i%T>`~Lk4!{kbGtL>r0zO6&hqX$Usd{k5{ep6>Gb#0>Mn~gk8Uo%e}W4 z-;^n@M>wrbwt@mnp#mKltxS@Hm0V7eUB{OeJ3zMx&k){cLP%-`xx?)OQ`0855qcA? zY*{9IMXUzS{=yP%oJ0w`iyd#b$}qdW1d*5poquAwzu5qaJeg zpv%?@hH9v`;4IBPWt{CCn?b(kKg>NO;?*y-DsDC2G3LugQ46qL3JmTeX}j%pp)^qE z(;r+!jr7~l$k!fflFDHN0vN#o4uT~btnBWUd{U9st(e~y5D9(dZ2WdGt zq#5XD^A=0b!#$o;2tazM!Y`N1*YvL5z^}I_VuAI$xZmfMD zaA3h&jk`n`iF8Z>tg^)~wp-~14MkB64-`@9ff^rQE1vLV?2~V!PpS(G>?I@Yzkch* zBJ=+mXAf1Xr&e#IdNVg3IeWOPoVw$DTYU4f;w{l0sdZVpU5&4>ipWoO{2~5BXtH}B z+oQOo#IgKdW&wsTR-#ZC=?D8$COhNY+}y{5s;;m0!#Cw@o3?goW$I-Xe3X+Tn8KCG z>6)DJWMyi0&BUDd!|&g}vEn_Yp1lP1Yp6mh4s&sQy*lJc@n#_`|J<~mm3Sg8tMHj| z?8pN{2UMCeR!tJ~Lr)>msCHCg4MdtYhcMP1mo8F($EGe)l zj4?#!%lA->=2%@HIV5Z>=f!!1!&M)nB8?T)_Cv5X9Gcu*j`eX%(b2VrnkXe*;c$Y&Ho{GuXTB4;hOu^Xa^UIHq8$1f^jB3Y`cCf+&>agUbf8Y$b-1^rWSnaO^4s83 zE2)wb$mOe}F`v$vZi9WpE9}wPBe|4-mZf#?FA|Zd$X%%5&{f9qsD}heV#p<>8b?QF z9aGGnQzF&IT!Nk2!uUqUg{2=E)dKF2EA2RC~z{baEU@#m=4H)h0d7RFrlNqa7IFwAnhV+~kj1zMU1Y zfLfTCUYMETIi>|=x-M^4*%Gb<6D3cvi-iojdj$L98$gQs-N&H z>v9x?BvDULzFVDS-yJRjXaq92v}A08ET950wo2y29wt4sWXxk>R7Qtd!o_A zm@L7ZbWu5GqmV2urbtU|N{j9(OHtD29YRsEQupFPMT_l@eD=4jimyRSpruS0k? zUzbEvoy!8?O2l9Fu;(l$r8oYj1PRiW4o~U~ma4qAwj@rQi$bMweZ=?3=uACj)!3Bm zjP0Dxm6!>giJduUEA1t5Nx^@YlQImo<2O5XTE5!<57s~_zXON%oo3Va)%@sR_u+PB zvD+Y1|!bLgZ)vqpiT}kk|t&(BCXOJSMH;xX9s*x*}8Pan+?}#&N z)o>xPOZL+kvyq#ZIX9ff?hDXAIL`UwN12$jaPnM{%Dl$8feMd4K1ep3qpP`&Dbc z)57Sa%ysQu#Ae3$=>5H<5+k&B)Z%nmsE>Ma1VuVK8aXv^49TfbhzlG%Il;z__59Xv zZerx*N$&r|4NOl@60#b~6d#f-P*Ng{jzV3#hix0G2{|=xy|D^K5PAC9ILVxY+8Qg> zE(>l~GE1P;001BWNklQM4Jv*+UADUu7@$x51;R}jovsh1o` z=w|MP%Vja=-cW8BXW*jndP4TqN){|pO~R5pzG-`7G3~o5niYe0vPyPj8O{zDIW}S= zRj`_tUM*D`T4jcoCJSv%0metOeC+ln9F9Eo^;OtxUaH*%^L#>3RqQSipQprP!Hp;< z$c!DJDpFw2wN+F_ebhEJGCRA-#7vw(xRHqGIA8p19jn8mNXk57xCfs%fU3li3o%Nz z5Ivm%7AA{4c05c~L_zScH}nH69~%VOKKrO{i#EmQUSj;ks)wSTzsdeKJjbdna-s!MT&8 zcq9cSd6MkhIcmd3hMa}DjGMA7Qc)!Qx}l1w$7-|Vb}A+zXKY1=&85%lGD(T@zJ*taf-4B+e2MelICj|vWUqJ7epAmCI5R6)!R5vL`Dvz}9=U8|@U0Hgb;n!x z5?{dzR-miyWE#F5FOn?1KAx|ln{kf0n zaC>Q5yOG+CRliVi^uqO$j9_-szni}N29$D+*tth(T>o(_)}L+(bO9bo$79hcsRDUr zY0eCCL;D$_w?7G&-d9^DAl#Bhw@0Jfz4YB6N#5&=Mzat%ch(%A!&~#-KKW&U)#o9! zV=Hq{KZl&jAiOES+YGeSG=he7-s2%<+^8r&^H`S)W#$Gaad^Fi8XMjaaQs2T2W>sQ z(=hg@x${p@xAsFO>GFYxAYgNa$S(|1N>Agj?R|e+F)bIQFIakL%l1P?NzNBqoE{SC=&a_+C(rVQ-@ApbmKOf_OHbqV=%fo0Q}bzZ$s{!mJ$&bTM|tMCVZQp+ zeI(K`{^l#VJYLqUZAQRKC>Q}z<)8oYdG5V$r)h`u%H9z?wgQ`4Tj}j>V8`}u z_U$`Gw9(3%b)BSBaavl!+ZX1?#7F)TDBv+_t zDqUN;De4-9vWndN1s8L$FXkZ$CMjUN9%F48HaVq= zN-kGKu~|4`PqW{Wq-YVS%2{X~bYP!y^7Vf>$(d6H{5B^IQISl>g4Y({o8K7Y=sBZs zUqfc$9QWV9g(n|ALUqK;zx%>&j=i=Y#ooe;`_3{oQeLxyl5(gOJA)&OWYRgFd8J5fW{$>4 zn4yUZQ}ZJA)hcyineLtz2F3;{q~p{#c5?beoaxydVVAH>DUai< zYQkl;Fw%d5M8S_p8L2EYIxA4BXp|=R(OBnWX7D(&CUWzxtt4j0+4sUBD!RaTAMrA` zn5Ut7k?x*4y4J6!s-Ygc-Nu>?wYWWAg362Bacc{?qK&bI0FukiC+~HU8{WsMgU8s@ ztI!%uBUke1!F8mIi&(v3td1r;p*lw5K4upMG|^)2H>zBrF{+SGYIr;%R-48`QpM+U z;BgsQ5JqN=PDL?tRunAaQhv<}QE&x8Da)Ms+M&xfAh}YZ_tQJBz?oUW3N9~VXQnPI zQQ%(_S|JKnu!0r5<1oFzRhi7p$+u<2ATGDEm$apVcUfENjx)QeFLXAE7LzRSMQhBz zX{v&%$mZW9qYKOHGZh5&-D_xAwGM~p7q=H#A_|s*16!RM!FQP`m>+x=n=8CjaC!?c z=FKPRG&pqA9>|!DoRL|u{I3_f(l7jesh!e=+lA#0EJawd7z$gV(W$ZCr_tqB3ETAd zG{pH0ST4lP^*u?jaf?}$dcVW4A*#FUPLc->Qd~%w9k`dm_|2xOL@E_zmy4XLE_Gb~ z6mrP8t(41?7#YJ?Q$uw}8=~|9wIKQcuBUer#{Oi4t9zoI@1Ga-3&OBpDrRPo%NdhA zv3shn>eutrpuZuTz_2G-vcEBkxtexCRj^c`lVt_DTtZ8hN$fw3cX1I(Rgmp=EIr-i zLK2U^HqCc_ILNN8>sjBsj&JBIb}o$|Soz}b ze9ZL3J~fbFW*YpFFdw@&%$jvhYJxc=ua9&=C*rX&G?65cw6Lki$)Eqf&oDn7;;Ub} zgKvHN1$J&}Cznst(A>iO)EuWyz_wkzguLVAOJU9pi+tu&*V4Pr%lXqM2~~&bT-AuK zDAae?^VpNe>1ydF6itxL71+9CHD^wa@cgT%2?c$KMsRA;hcD>DWmlP+oWa<3d%BxQ zXVcWwiqzEU#OGpER|lwXsX@-?v1amEWEoMdFqVO29$Y~OPWw_p+V6E@GeU|4Lt!A9 zsxsr47{}vkF6=Hrzd(0f{7f?gP%RQ!BlAKA!6GrPzZ zKBaNdy2@Be=9eyzD=3s@gM_j%m$ovxV5g)=SS=9sm1zt^E|X_NXNYrCi)`J}&c-#h z436eaYpHO^$H7wtvhxnaCr2|paF>rY%^qeJW(Y*9 zX=vKa*ysTL$DgNn*KVQ?1+|c6Zf=@28@J*2Kw)8=LMlsqVuS(19&PkIbs;}qONP1O zv-C8T+1-4aTh}JZ=jX8mSCd(sCJ>5{UCa>-JMq~{G}qXen#}OuAF3g?kj0mM30*5A z+iOuY3yR>vTA9TijFK+7X^0x{UJErrk)zUY_Hf<6x{jN3Q;Rq!3tLJcHj&(GvD{5 z*}eOAtQaiIRzC(=P%?v!F}+gXT*|XKocOD1i8i;JihvEA5MYDkQvkq5wF-bm=G*tzmriKr!4bca2 zJ-rh!_NTlZT$jP7IOv$0aq%urCL z-Hy{`L#Y^66B4$v!osVk@GmSNDKeVf4(%<>`Y43B~aN~M*@2O+mDxI1-HwC%Gvrj&SDp>ig2i2}3fAq)qv44Lb z(NKWiwe84?F)t!*?QP7)Din%o+%5@^%Rwqz!Q-r>u|AB`X*a#$tLmz;NERkz^LTwO zNEfgai`aDqt6Lzjs*OTL!R8Q6#fDr~aM~SIWW^*4jN@St1WScnVY$?Pb=eSIP!s5v z*bSM=2tlMQD-_Ez7K@$vc?H>`b68&BfHO zvxDxAMl(Y~j?MAWyEb#;&_427iC|TbJ)66Ewhs zGncTiFgr%yV2p@A!Y3YR<;lG#898^1KmF5>asJc+_S~=*xst*k@bKKeX)JOp`wt(a zw!uzoYXh0ZaVlkvny7~4b|6X$dPSwa!$Wtgg}ZNFjV~PFYhOP}eYHfSR;B-Z3}>(r zm(@kcE7K6Q6SSo$xHJ-8iA7N%B`MU34s45H(S(ax5C)lLm5iJb%_Ivh#g)FMmbj&* zcYt9dWb9GK`>|A7(%5Em3bJY;ms3cjROX9*<}-pBbnJJitg0=VpK}AVn%Mz=*KQYc z^9!6kc$m5AIfhd%{`^n-$;&oWP2!2ChRNjeoH?4JZ(QK7|HoNw+ZCiHERjynGrdqk zb5vomX`JoPanH3$>TDw%f8|-MRy$qw3GB)kXm%E7`uMBA*vmgW2#JisD<>50TRVg- z`uV{ld8SJdJYt;c)z{F~(||kLWfEatPl(!(g>38?A%}w9;X=+YP!)+13WHmBetdcTs8&4 zPk~X0n;Uo*f6bb=Y-x41!xjn<*>fGmiAhp}LqBFiWWcIv1XLB-Y9(PDGgbRBVvS3( zCz7!_62qfJ+FA(K)&5MfzzDj#`1dy_1YYI%Ag-rh6vqA(Zr*{zyK+tUW>_Q#no>4Z z1*%-Y6>Pq;2KWo&g4K{g3~)h#rWi^B(G09K3Ub*@8KokSOy<#ZCDgtl91}5wTprQw zLT_#)>8|Fxk3Gqz>$maB6X%(m(|P)tGYAe3Zhw)IGKbZ%NL@`UN6th!ai&0QaFWmb z-c5vE9_m}V%uvwkx@yXW6b`4%_{<2MU13(WN0=I&;JN3<=8zcp;>CYGK_V5!?`O~E zC-K;WN5~ZkMZIdI0*VQ%gY^1w%%*uH%gKYTRF(UYUhPsh3Wjy57uolJa= zE!WhNsN~TE37@sd%uI@o`fdEdpB_NB*clz_r&5TszBf!=eHZ6Ol7!r4zWj$DrERsJ z$DSTw{kkZ*q{?)vfT#y>+We%GHqH;8LaaFW{*MMZKVD1UU>{p|1ZWNU=xvLVD9D^V zHG|(;O<(^!!xIHEYKR+lH?Vm_kcD`V*||A9_A+(R5UuTg!fqd#i8*@7VlqE%kww&~>X#&NPN~NNcE9mC?CzI2e z$$A)>v!LiAk}g9zi%ZV2ZhZrH-PF#h^Ant#v@stqbKRCMZra{Xy;tML8{3$e8!+w0 ze9ksDuZz&RCc@E!bA0yKyJ%`#&FJ7U()k5~>wbfWUMNvtmoQaEFC29+mZ;FZs*|H5 zUYeTxy!J{T-nP3rc%;lXe-uQNplwYXJJvg?4=dztU5pMZ?77a4H{fBge}vBVCQcrB zl`nrIh0l|x)entfFA2E=hf+b&U8J&QRPQ$So{7@BZWFiOvx!jWE{@IZV9WJ88JP`Z z3%4^ql_Z@?;Sl4DoH2sd7umf%M4_TFFj8j6hA_A6aZ$`Huwi`-vSKA=glek-W!1)L z{{R);hsR#Q;R&KESx_qo78?d%E*aN!HY;*PHZxULh=MB&!!MtsIG(#~gDS~IR)1)v zAi07ST#EQW?6MLC?sne_QLus)tl(XP!(U5!@~Bzbf2&pu!g4^b;khq~OI_27p)Al| zFJ|g*y8pY4CBJ{cwB#{5?)~qQq97AHOKxG1x;6LxLXUG{P7H;CSJH{vmWY7{heo|a zqtm@a4z##5^KW$yovb35KTE=UOTxSz7=<`pl}Sz=#$WpZvCT8=J?u6Dn>Jwg`AM8O z3FXQg?qaM)!}r@`C`24iN~(&kX+QlqY8s0(vrL}rr*Z8Xg7vlN#{bANaw>z8&7+ko z%s&1Ux~vj^{w0c&(^&mM^GaNhEbj{?!Uu9a{Q@xdr&4B?s?M9Q+Sl-I!yuoMm6nGfAXbvS}uD&q2nId}FncivphwrgCx{Orr< zXiN+YF)^6pNWV_dDVl}jKm5IqGcmrvORpa1lb`-LySB7&zHgYn`}RHxr3eAhi_78W z)q{)F1e^riZ9MeDaa`4IHf->)YfC4YScY1EkhRTGK6dYRe)o$vVwI=y1zb?cvw33+ z7RgI4mn0bRuxU#zP0_XNJN615ONKB0{)hOh|MobF=3w-AiMdRLlc)N*?7GnZ*0QD7k>Ay*vO@+sVM5sTT?t&x_T#LPhC zG6<64yNKH=%VRxwzePR5jT4hx zWq#}FgBT@xw)RUTCiJV*sOkrMzhG84Syg+d~uRQ%ERft3^f*rxD=+G7IJo- zibcg+mT1R`Q_={BT&7iwaV(5X2vyUUtvB_HsbN>*x_X_^SRyse^CD3+gTEC;Hw)HA zBh6w`AzxIPO-hU{*eS{qf~L?I((zggq_Y~&?;T~|zG<2pC9dCI&wQ-FpL}J4FMVSY z(NZQ{<)Eo9LThsX!4@aBFwXRh18ccLSxgeK3rysDiTKM{N+;-9(@1T7Jwk4XL$6LV zo)KsaYY0lgB$*18Mnw52;b1*g{t9(EkwmR&Se^@_GaksJ4M(f@$eH#9y=^z$&RtAQAJBl5=(1Lj!$y($Rtlc_%K1| zI4#~$9JVALZ63GJO@8(mc4rM1M-Ap_6gh zci$qsd+Gw=k}snqstN*Li%zFo#r-ZgN@!|@nX^BlX4PF-o&I!(TRT~o>%8WL zlPs8X`)&~+?<9;u+~U}NGlbT#kNKcL@_Gnt+<+WUkRP8wE0uoimD|jKV-?-uAZ1)A z{|vIg$gxN-COP@Y<80ZsjnwJ$4E)JoGWWvE4E}$A&Cvh)-^8DOj{d*@I;mFVOlZRoF-9F&duYkTJ^q|DP4cv z%El-b8U;9`6sO1kNMlz*Y+S;nP>y~TK3k%dXxAV+P!wepo z;_wlj_oa^F#A zr;^mwSXf9eu;scQLIF3eZ9eK69oTGcVlkaWQKhT4!~-8%!yo_2(|q_7TNxT0p}N|G zs)_gmZsrnk+;#_&)PSOusjpJV+55C&rJy0A|9WeN1i@LON~fQv6IQf$z>BH=4aV`-6l>R>t|v#PkVcWLUxYd{N!$) zdSMQ)Kaa=d;^46qeqVr0u0(txL1S|@XZmJ|R@D-5+Yqv8;#z@Rm7TJr;dR;A;`R}> z+RVbPqJqO=rC3tTV*~@R1(PJum)4OHnPQ}MWwR{fw%*}>49 zjYPq2QXo|Uo$iJ_vFRM8Y=wZ^M_G4}EmnvpON@==_|1l?-1_m`X!lQY=g05HX;TQe z6&f16=z>CbXA2SW6pfKQJFnYj23Aj}BsQ$6VfyTgc)c~0W{)9AQ679L!oZB1#_k@j z*j#g+5 z<#G8VL_-ebVunn~Ml5N=DHW;65{hcUVOz;4xZ{qaY6})ROir(eSET#1H+qPmb3fgXI$wS}B?K3V} zR2|c6-@2T@D;gIrqVPtU!7m8mOdS&hN8gsE;Lgo#`mMjB_eItz4H(Lb6~eX z;3^;qxKN0*I+-7Qj$q@~D_}(L9gI$X@2W2B!4UDoN1^iL$^wU`;g%%IqDVno>I%HD zhD{4EQA831T7yAaMUkz!9Mg|I$;fyAg~HGX*}i_1Y!9GxrvdE6S3mxoDwSF5{9Uw3#^e(IKQ{%G{LHh@a4|O$fEsB$JaII<~;k{zW8F zr>3ceGv^1eS}o{S3pSU)lLwA+=JX^Z{Tap$l=3hNY7`8r&KIjo@+L*#^r41 zmA#`(&QsRxY!Qwhg001BW zNklF+PFv8k5tKE96!Upz^5$i_q(9{k2EBXe;+ zd~X*vMdNdy-NrT7cVMwQXlrtiE%fosp$VL_PGhtIheN{e5s?)a6ETr&K`|m^C>N$l zOegRJ8=07$#%i^h3JlFqV&v1r=4YA9XQ>JW(1a3$lRBB%IE`+wC<+dnhP}#$!|y;8 zGy*{tB1V=V(`l8j7R=;~~srPYVz6UZ%2VD*%V&!(B0bx^9b@RhGT%00K$aK~*8%*@Qu zvuZWT^gJF3Rq zET8-2hd6Y2iXT2VOk=2y-J2Sj>>neq3V7WPe)+>&vFSRi8zrvW)y2&>HqcxZq?9Ss z((1LFlt%GZkH$(6w`9^La4E@+^sKMfwoo`$cs0qjhAmQ){`h% z&@>fA6DccJPL61d%)8920*}iGJx{W(OQNSIOnbK#-M&aBn_+Uc2B$U8{=G3GH98x& z`so{rGhcEr94j$)Hpa+sn&zemciz*>(=Yb%^4<|b!2(umm{_VpLm*A6yoPT)*vFCB zCW-}xWdEx~JF01HXrx@qVi6q3iFsxQkJ8+;k?LTW{^uX2eQOt6+AL^`GdSzJ36dqX zm|(}v8<`zD$zsCJ^voCy)n$xss!Az=w`L<+{xs8UV&wc$PL%Iwb=5R>n~u*Pq$=cu z@*-`Gb=a#q$d?qXf)l@`gjJGFdm*bRp{g>XcQx^WgJ#wWe zA}hNnRg6$)FCJ!D)oBEY%UG=*Tn@`}5WNGxTQk++`O=E@&=rSbI?u?1=P&E97LU!- zd~X--@Jc7j3RYksJ(XbO!E={wE^JW;>wab1%CT6%3Rdv0!WC*^_Vj~zBX8e|!MGN2 zS`2l;Qa8NOBW+yjl@Tw7^9mQ`#&1*?yp`*iH!BA&y^Ra%1wl7+35D!cZMd-uzw zG_k%XXxi{gm&GgnYIB`83vu&(Xqt@ES9b-B=)Hs0@59~EidHO>8yH0S2?57eMIm62 zYF;m?QVG>89*KBtc3Ny!R=eHQWHLlbB}#_>bGd9Pbc8pp{Y!D-b|V10I2WfBi&1;W zt?!G>f)Da~dMhyYr@8*8sayLYM9FsL5BLgz-4i7<-$yAmMWAl|Rr@}9y_oyDC5&(} zCrB_YJq#Nhx7*IbLXs-ub9!hL%lra@rlNX1$X#uW$tuZghQz`cvRFq|pbJ^G^Y}xf zc%orCS|Z$W%U1sV|MkE4^FRI^b#=|uud1S^!9#pDPAC|`W%cpe{&Tc+>|k-~EYIve zz?Mzz*t9Z}V>8@+dlwtmH1hpN_wqYmyqjDs#x*-P@!Zq>eC_Kaj7^B_ed>Gs^WXnx zs)J43_2EB5vDI_^)YLtpAijM{|#R;}t)4|B- zdG@|GOMicYrdk1u!-_ws@dv;EVNM*FA)eHkjr-`ISfI8pL}RU;jt)OB?%T`ybz6Ak zp#;@YC$-T!oP;Th8D{5;6c)X-*3~mTK7`ZhH& z)6zh)I7eqw4Xf6J-ySuK>0?uQY8#_8L_PGMOj6V6!RHlt_=hPDosCmW<@xk4uf<_0 zP+AcAhi{D|3y@CdIeaj|<4+ZM`RP+!zk4mR>?7z0kKe(XCL4h|Cvq{tuiV#4ZL^ob z=@cKmWg8#<<^M@2x{BGv47pT+tsDK^d`%OJ`B|!}{P^4w!($2hM$@>w3R6=u|Mbvt zP7Dlk%a$GIog8~dD+KzzjKx^e112_j-KR(o3}Bw0Fg*FZYhP!;iS2^0#V;#S;HY)25Ctn(!3y4WEET_XQjw(iNL$63aw^(dD9D;;%3Mgu=ggNxs3G3!>ti$bk#rt%8n2 zfJUcgstMi=+oAdXXH4r8Uro_Bq4h#feD zsH$&xRmM8D8_EJrBV)H?bvkLbT3IE_L^Tb2K5t$ZDF4Q}&=rNu$T;5CR=lgb-WPNF zL0(Tk561rFjdtP=w_n)pbPX6|Pp5wp%4X8iUsTrCju3od+8LP`i*<|!WaLVqbJ6x>+WDvuZ9IK)qB6VI`}u=^{|RELIRDRoyq{cQj$}5)M7)-b?QN`DU7~xHi@h%-d2ydga$YCY zRfj0Z+;P)RUfp+&p2ilQeQk`_4i@R3Dx2Y+x|(3)8XLLv95vM*7V;L3?pJZSLdfL; zcCUgYl@Y7~N|hr1po>^erl+flV6YmeJwqvxqFR)Z3{qDsld{{e1bhgJhTH2TohjmU z*-?}VHk%dEXs1}NzMB?ASFo@A66>U2Jm)ftyaE*RW%HbDYN|j+6PUIpdC8sTpf|;$ z9Xz_k_Hu}wGhX)8G_bKZLSwU=yLWezCBx|OB4t_PiO0`#a%hlaWBuH?a~IDYI|z7b zZ}t*uEwH1lnYo1wW79Hy=U*o1ZDUc1a^EfW3{Q@6!!6hF_(KQywO{HcmKewHtmTR4 zCz&bej0{xRwz-G0R$<6^KHb5yPadGX&Oyj8F*!I&b#)L?c5wR*TR3@q0#A*Lk-<^M zM#{*UAg}e08R;+l>PPnAbg4{dhv?|)qBb0+-fd-gWRatN8N9wAKYC^yw-Bb*7e$=))5VFdHi|Hx{F1RV|M_5#wKEUFa(hTpvGLNALZX=9U?6p2==`1-lC@Oqxa(jFnaAxQhupMT;ZztH*r zb408^?=gS!ZOdenzI07cE@N79DHR2|Y$KJov-gC;Y|3Uf9r9yF4IhRjfIy_D>S#& zkxs?fzGnx|{^()s>3MtsCv&+DP7P#e?5xA4&111bQ$ro6#_XhXqqyxl{)P>db5qER zGi>e+v3rX^Fxo*`F44Va69W@I9(w3J7BUnZ4H#;qgXQIG&mQj z4)|zp?c%N*qU^q|m-aPlc;TrR>C5b9BBqd7%#g8d204w(<1mQ=1t-PqJl=3ERbd;t zCYS;2c83E*n`y5miIo+i;7Wttgol+~I$-6@x(*7$iZ^x)jpuv+CGI-7H{Kv?aBHAiPcz2+JMT;Yc{(>hHLkpqQF8Z9N({d}6r+a0PD#Mj@_J zNRVF~#S`iLz|EOqL*!fAgDYA^;>b~SBjEUwt7hXTr>f#{IH;{us4Er`3_`)M4EaT$ z+jvi@DwD75qw}+$F+)19c6`v+(@%r3KP9J+QQLFR%Kr2=;WUDcCl6Ch&k(5XMG&vP z_laPM!G<8H7nOa~nIZq&1;Ok#I!XvMW{LZI3NXI8=?r38&K7n1YVR&ea zN+r!lKD>*Q=g#8wxcSasJ!Sf~FUH0U29-E#wf^k?_;rT_eo z-1D)ul;hJ#HZMcNGO<{eXgETpFvUIh?&Q%Ien8kMa$;zR`BZ_m>w6g*8l-oflcq*1 z6H_q+vCQb0Kvk`ah&O5Gz~mBRtXo%&sJq#JAda)zN<5Y%xtOK9-NX7dE?m9}_uYR3 z|L1@CHvi%C*Ym=Qll1i^Kq!(el*p$ix&7u%?0s#L+>DiqFpH$8Xm6<{Q-EMOjZKk3 zE)!}9QFKT+d?9j~Ji20~A}d5AF7u2>vPuXd2$!7Y2+N%jR|;34FI&P~8Zu=Rd5x?E zV_g{`x`mQLMo}1Wq9=NlG*Y@sb{krWu{M;Z_ z(Pq4!YQF!Y5#k9uFC7@d>8K;GEKn1O@XZG&sIqB{&Km{z0`n;^3$q!zJL)-de3dEpz-s-%L4RFVBD`UJ z=x=FPP7qMFuHmtJZ zaU|&Ou4aB_5u2Fho@-YT&k4MEIE}8$^be)kw#m=BwY7Zz=@B-x2XTm5`bW|zx{GWw zXR2Ua&L~ej*GF%MjcApd!>^siR?j6x}g)!D!w z{F(9CVJ4>MsEZmTiA;L>7;aApi_J!U@gTR~)`cQlDaOvw+UUd`tYf!fCN5i zsi<#ArOg7rpqt+f6g1H!3XK0TV+;f?Uho+F!V;UXbb+ET|6LSxQ@K!W)2XxTCP7f| z(C|x3UjFasg|@Dn0iofh9XR|emM3o)Mj>u)@Ok{zs}ZHE-!K2|!YI%OHuNGnoy;FO zhA3Blti{e=sbDh-eDCy=8;XQtiOj(x)ZKR%qRsj)y~_6vAN2L~V_4pwjNBB%o@oVd z4~CM?@I;qODU@;scc}HMo571+ySi@H5{yEjAXq4s6;w^YB1)#?yE;CDWnc^`n=_St zawNihsGgBU8~wxcoI3mpdBw~5!D$|P;zdrLAH@;0;RwRk9qlyNG;{jwX=-Y#7#*L$ zQPM2&hV#y_V4)A@85u)cJq-h?7&g5k|<<3 zGdyY*Js*4gRcfNugc|I$)Z5swt(*00dzhb^MzlHEwAPNSrA#mO&-~U$nNOB+_(azB zL|I6P%$*lmtPHa-HN`DA^m6M>tMON7*ts)=D9v&29bF8K4dC~9Xsm5yd~%x3rVzb7 zA=a(7v7y&PV@rU%qH^-^DRyq}!e$iHN=0nGGShLHXt z>8D;7KRPZU6fW)P=F+B$pTsu5>G6MB2l$2mzo^_a)|M!usQU8#1S%DUvQ6iNl;)*C zj)P*FA}St5q-)Vh&Av1neQk^nrf^$onM=nRno1I1ED&yNBOWgymK#_|hIoGO5$?IM z7d@Gyqa}=8Pjk(+tC^qMM=6t|zM~4i+sW}$(>(d)%Y5N;d$0;I7E2mCw``$*w8TPe z2CeL7U?j%JKfa5-&l|q&9i+2XX2wTot+P;56XDdkMWUe)Hc{i*{RzgV^Qe^!HPI@* z{m2P^=kvQMWac@2YyzjvOCf6~D^HTi7TB_GHDfbzUOlwH^kjt%YrHr@d4etnlZg^j zBSq%p<8<~mGjwc#Q-cLQ`&-v=W>DkHUp~NwEfMD8*<}}lG$RIjYU1K(3;c%ZsUbP!90JV^%Gg72E zEHgNxFqfRAypST9FK~V^MSb}HXYaj(B)RV^&reomd2hSgyXl$f@ebnw%)k&JNDy=) zMM{cWnUGGWTj{i`JKZVb_DSYzpVV#ue#p*e&367{<^}oODp86OBkw%#{;Rh z7M^_S4hQxp5p6ZpW|LIJM?7T1h75K$&s=t})QIr-BR00}+JnRCrEPE*b~D38(+DNP z+_>-@sj!onPvn`GGq@5#`a8Vz?(D?t(s}u%0?8gf6SK3Fs|vZ?2nUZIq&;Rww|l5p z3fR4I8YS4i!^iSciIEY=E$0z}1DrY$re2m<%++v-D(U4p;@&1*+xH@BC3bchw58fm zb%jt%H@7B=_*-|Nx%aWA9^}9KwU6`AV-Ilk;s~N>BcNvx+${w41+>x}9sVL>b&mbJ z{KP{(N|G1VFf$;`yc(UTSH&A_M{O)oua~h2PC`LHO}Vxu6l_M6*9z}?ps@uV=3W}7 z=g}Q_!ro2zN^M~a?+nT-1t$Lf-4hDz0g;2hdTQ%zZ($2tct>CcJC;|5(N*~;wqh{9 z`!WY;Gk-8}*V4-D$gsqx4ZF(8#P4d8rBXoB}(1j}3h9=RqWwQB6db(RUb?OBF;oqOJEMR74Z!xf= zi?uAAJl2n{sOUB~O+hAG$r4ZQ=9hl)`@Hb%6@tMv7O&@c^+kB&wFM?;=W*Er*hC-u20es>Med9l{HHHoAk^MZ)T`0c6Tt6v(7!## z?Xd|?o<70iio~TGlVs94&yOBG=d`69176%sPJbJa2BZ5njArmTq>tt&34LLE~vX&)3~K zMyMhQ8X;NZNHNCXc$m^1oep=1KmY3sG=%*O42UdMWm0|{ufCk)D}OzV=xSwkWrb43 z&AB(e$Bu!seE->Lp1pL3<#LJN{_W4Qw!DZQin6;a!L9LCwr|@`Pp3>EBr-j3aR$*7VRfZJQ@g-z4jH?%gNhu&>xpw`WC_t#=ffX4gf|%E z{Oh9}**n0�uMvguLtF()>sO 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

0EG&&P?HWdbg6t`=Z>M9dMqjq}F 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%0D3#%a=uhcZmE{imt2+zx_JZx6%3ZudSZ>p3gUe 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 literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index 2d3df2e0..b240546e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,49 +1,45 @@ -stLearn - a downstream analysis toolkit for Spatial Transcriptomics data +stLearn - Spatial Transcriptomics Toolkit ============================================================================ |PyPI| |PyPIDownloads| |Docs| .. |PyPI| image:: https://img.shields.io/pypi/v/stlearn?logo=PyPI :target: https://pypi.org/project/stlearn + .. |PyPIDownloads| image:: https://pepy.tech/badge/stlearn + .. |Docs| image:: https://readthedocs.org/projects/stlearn/badge/?version=latest :target: https://stlearn.readthedocs.io -.. image:: https://i.imgur.com/yfXlCYO.png +.. image:: images/logo.png :width: 300px :align: left -stLearn is designed to comprehensively analyse Spatial Transcriptomics (ST) data to investigate complex biological processes within an undissociated tissue. ST is emerging as the “next generation” of single-cell RNA sequencing because it adds spatial and morphological context to the transcriptional profile of cells in an intact tissue section. However, existing ST analysis methods typically use the captured spatial and/or morphological data as a visualisation tool rather than as informative features for model development. We have developed an analysis method that exploits all three data types: Spatial distance, tissue Morphology, and gene Expression measurements (SME) from ST data. This combinatorial approach allows us to more accurately model underlying tissue biology, and allows researchers to address key questions in three major research areas: cell type identification, cell trajectory reconstruction, and the study of cell-cell interactions within an undissociated tissue sample. - -We also published stLearn-interactive which is a python-based interactive website for working with all the functions from stLearn and upgrade with some bokeh-based plots. - -To run the stlearn interaction webapp in your local, run: -:: - - stlearn launch - - -New features -********************** - -In the new release, we provide the interactive plots: - -.. image:: https://media.giphy.com/media/hUHAZcbVMm5pdUKMq4/giphy.gif - :width: 600px - - - -Latest additions +stLearn is designed to comprehensively analyse Spatial Transcriptomics (ST) +data to investigate complex biological processes within an undissociated +tissue. ST is emerging as the “next generation” of single-cell RNA sequencing +because it adds spatial and morphological context to the transcriptional +profile of cells in an intact tissue section. However, existing ST analysis +methods typically use the captured spatial and/or morphological data as a +visualisation tool rather than as informative features for model development. +We have developed an analysis method that exploits all three data types: +Spatial distance, tissue Morphology, and gene Expression measurements (SME) +from ST data. This combinatorial approach allows us to more accurately model +underlying tissue biology, and allows researchers to address key questions in +three major research areas: cell type identification, cell trajectory +reconstruction, and the study of cell-cell interactions within an +undissociated tissue sample. + + +Latest Additions ---------------- .. include:: release_notes/1.1.0.rst -.. include:: release_notes/0.4.11.rst - .. include:: release_notes/0.4.6.rst .. include:: release_notes/0.3.2.rst @@ -57,9 +53,10 @@ Latest additions installation - interactive tutorials - api + interactive release_notes/index authors references + +.. api diff --git a/docs/installation.rst b/docs/installation.rst index b27a8f3a..4e18ad51 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -6,7 +6,7 @@ Installation Install by Anaconda ---------------- +------------------- **Step 1:** diff --git a/docs/interactive.rst b/docs/interactive.rst index 459d213d..54e85f49 100644 --- a/docs/interactive.rst +++ b/docs/interactive.rst @@ -1,12 +1,12 @@ .. highlight:: shell -============ -Interactive web application -============ +=================== +Interactive Web App +=================== Launch stlearn in your local ---------------- +---------------------------- Run the launch command in the terminal: :: @@ -14,5 +14,3 @@ Run the launch command in the terminal: stlearn launch After that, you can access `https://:5000` in your web browser. - -Check the detail tutorial in this pdf file: `Link `_ diff --git a/docs/list_tutorial.txt b/docs/list_tutorial.txt deleted file mode 100644 index 53badb09..00000000 --- a/docs/list_tutorial.txt +++ /dev/null @@ -1,11 +0,0 @@ -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/Pseudo-time-space-tutorial.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/Read_MERFISH.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/Read_seqfish.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/Read_slideseq.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/ST_deconvolution_visualization.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/Working-with-Old-Spatial-Transcriptomics-data.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/stLearn-CCI.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/stSME_clustering.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/stSME_comparison.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/Xenium_PSTS.ipynb -https://raw.githubusercontent.com/BiomedicalMachineLearning/stLearn/master/tutorials/Xenium_CCI.ipynb diff --git a/docs/make.bat b/docs/make.bat index 2afd47f0..954237b9 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,32 +5,31 @@ pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx + set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build -set SPHINXPROJ=stlearn - -if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ + echo.https://www.sphinx-doc.org/ exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd diff --git a/docs/release_notes/0.3.2.rst b/docs/release_notes/0.3.2.rst index 9b141ff5..d8e459c7 100644 --- a/docs/release_notes/0.3.2.rst +++ b/docs/release_notes/0.3.2.rst @@ -1,7 +1,7 @@ 0.3.2 `2021-03-29` ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. rubric:: Feature +.. rubric:: Features - Add interactive plotting functions: :func:`~stlearn.pl.gene_plot_interactive`, :func:`~stlearn.pl.cluster_plot_interactive`, :func:`~stlearn.pl.het_plot_interactive` - Add basic unittest (will add more in the future). diff --git a/docs/release_notes/0.4.6.rst b/docs/release_notes/0.4.6.rst index b2f08dd6..b8ee0324 100644 --- a/docs/release_notes/0.4.6.rst +++ b/docs/release_notes/0.4.6.rst @@ -1,7 +1,7 @@ 0.4.0 `2022-02-03` ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. rubric:: Feature +.. rubric:: Features - Upgrade stSME, PSTS and CCI analysis methods. diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 48c9c3be..6194c62c 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,10 +1,7 @@ -Release notes +Release Notes =================================================== -Version 0.4.9 ---------------------------- - -.. include:: 0.4.10.rst +.. include:: 1.1.0.rst .. include:: 0.4.6.rst diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index a2c20d28..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ --r ../requirements.txt -ipyvolume -ipywebrtc -ipywidgets -jupyter_sphinx -nbclean -nbformat -nbsphinx -pygments -recommonmark -sphinx -sphinx-autodoc-typehints -sphinx_gallery==0.10.1 -sphinx_rtd_theme -typing_extensions diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 0c0ecf16..3c9a62cb 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -4,33 +4,15 @@ Tutorials .. nbgallery:: :caption: Main features: - tutorials/stSME_clustering - tutorials/stSME_comparison - tutorials/Pseudo-time-space-tutorial - tutorials/stLearn-CCI - tutorials/Xenium_PSTS - tutorials/Xenium_CCI + tutorials/cell_cell_interaction .. nbgallery:: :caption: Visualisation and additional functionalities: - tutorials/Interactive_plot - tutorials/Core_plots - tutorials/ST_deconvolution_visualization - tutorials/Integration_multiple_datasets - + tutorials/core_plots .. nbgallery:: :caption: Supporting platforms: - - tutorials/Read_MERFISH - tutorials/Read_seqfish - tutorials/Working-with-Old-Spatial-Transcriptomics-data - tutorials/Read_slideseq - .. nbgallery:: :caption: Integration with other spatial tools: - - tutorials/Read_any_data - tutorials/Working_with_scanpy diff --git a/pyproject.toml b/pyproject.toml index 74d75b7c..0e4c9865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,10 @@ dev = [ "mypy>=1.16", "pytest>=7.0", "tox>=4.0", + "sphinx>=4.0", + "furo==2024.8.6", + "myst-parser>=0.18", + "nbsphinx>=0.9.0", ] test = [ "pytest", diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 6e982e67..8c568a56 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -33,9 +33,9 @@ def Read10X( In addition to reading regular 10x output, this looks for the `spatial` folder and loads images, coordinates and scale factors. Based on the - `Space Ranger output docs`_. + `Space Ranger output docs.bk`_. - _Space Ranger output docs: + _Space Ranger output docs.bk: https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview Parameters From 4e9453b4bf375ce1664860629229a094b8d66c40 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sun, 6 Jul 2025 19:17:17 +1000 Subject: [PATCH 125/241] Fixup documentation. --- .gitignore | 4 +- HISTORY.rst | 2 +- docs/api.rst | 210 +++++++++++++++++++++++++++++++++++ docs/conf.py | 13 ++- docs/index.rst | 3 +- docs/release_notes/1.1.0.rst | 2 +- pyproject.toml | 2 + 7 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 docs/api.rst diff --git a/.gitignore b/.gitignore index 6fd78a11..fcd9e498 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,9 @@ __pycache__/ # Distribution / packaging .Python build/ -_build/ +docs/api/ +docs/_build/ +docs/generated/ data/samples develop-eggs/ dist/ diff --git a/HISTORY.rst b/HISTORY.rst index a6c64446..8c608fe3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,7 +15,7 @@ API and Bug Fixes: * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. -* Removed datasets.example_bcba - Replaces with wrapper for scanpy.visium_sge. +* Removed datasets.example_bcba - Replaced with wrapper for scanpy.visium_sge. 0.4.11 (2022-11-25) ------------------ diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..104ad279 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,210 @@ +.. module:: stlearn +.. automodule:: stlearn + :noindex: + +API +====================================== + +Import stLearn as:: + + import stlearn as st + + +Wrapper functions: `wrapper` +------------------------------ + +.. module:: stlearn.wrapper +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + Read10X + ReadOldST + ReadSlideSeq + ReadMERFISH + ReadSeqFish + convert_scanpy + create_stlearn + + +Add: `add` +------------------- + +.. module:: stlearn.add +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + add.image + add.positions + add.parsing + add.lr + add.labels + add.annotation + add.add_loupe_clusters + add.add_mask + add.apply_mask + add.add_deconvolution + + +Preprocessing: `pp` +------------------- + +.. module:: stlearn.pp +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + pp.filter_genes + pp.log1p + pp.normalize_total + pp.scale + pp.neighbors + pp.tiling + pp.extract_feature + + + +Embedding: `em` +------------------- + +.. module:: stlearn.em +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + em.run_pca + em.run_umap + em.run_ica + em.run_fa + em.run_diffmap + + +Spatial: `spatial` +------------------- + +.. module:: stlearn.spatial.clustering +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + spatial.clustering.localization + +.. module:: stlearn.spatial.trajectory +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + spatial.trajectory.pseudotime + spatial.trajectory.pseudotimespace_global + spatial.trajectory.pseudotimespace_local + spatial.trajectory.compare_transitions + spatial.trajectory.detect_transition_markers_clades + spatial.trajectory.detect_transition_markers_branches + spatial.trajectory.set_root + +.. module:: stlearn.spatial.morphology +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + spatial.morphology.adjust + +.. module:: stlearn.spatial.SME +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + spatial.SME.SME_impute0 + spatial.SME.pseudo_spot + spatial.SME.SME_normalize + +Tools: `tl` +------------------- + +.. module:: stlearn.tl.clustering +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + tl.clustering.kmeans + tl.clustering.louvain + +.. module:: stlearn.tl.cci +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + tl.cci.load_lrs + tl.cci.grid + tl.cci.run + tl.cci.adj_pvals + tl.cci.run_lr_go + tl.cci.run_cci + +Plot: `pl` +------------------- + +.. module:: stlearn.pl +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + pl.QC_plot + pl.gene_plot + pl.gene_plot_interactive + pl.cluster_plot + pl.cluster_plot_interactive + pl.subcluster_plot + pl.subcluster_plot + pl.non_spatial_plot + pl.deconvolution_plot + pl.plot_mask + pl.lr_summary + pl.lr_diagnostics + pl.lr_n_spots + pl.lr_go + pl.lr_result_plot + pl.lr_plot + pl.cci_check + pl.ccinet_plot + pl.lr_chord_plot + pl.lr_cci_map + pl.cci_map + pl.lr_plot_interactive + pl.spatialcci_plot_interactive + +.. module:: stlearn.pl.trajectory +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + pl.trajectory.pseudotime_plot + pl.trajectory.local_plot + pl.trajectory.tree_plot + pl.trajectory.transition_markers_plot + pl.trajectory.DE_transition_plot + +Datasets: `datasets` +------------------- + +.. module:: stlearn.datasets +.. currentmodule:: stlearn + +.. autosummary:: + :toctree: api/ + + datasets.visium_sge + datasets.xenium_sge diff --git a/docs/conf.py b/docs/conf.py index 4d08b253..fe622465 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,6 +22,10 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', 'nbsphinx', ] @@ -36,4 +40,11 @@ # Configure nbsphinx nbsphinx_execute = 'never' # Don't re-execute notebooks -nbsphinx_allow_errors = True # Allow notebooks with errors \ No newline at end of file +nbsphinx_allow_errors = True # Allow notebooks with errors + +# Autosummary +autosummary_generate = True +autosummary_imported_members = True + +# Output directory for autosummary +autosummary_generate_overwrite = True \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index b240546e..c1fb1d1f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,10 +53,9 @@ Latest Additions installation + api tutorials interactive release_notes/index authors references - -.. api diff --git a/docs/release_notes/1.1.0.rst b/docs/release_notes/1.1.0.rst index fc7d97dc..9d040781 100644 --- a/docs/release_notes/1.1.0.rst +++ b/docs/release_notes/1.1.0.rst @@ -15,4 +15,4 @@ * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. -* Removed datasets.example_bcba - Replaces with wrapper for scanpy.visium_sge. \ No newline at end of file +* Removed datasets.example_bcba - Replaced with wrapper for scanpy.visium_sge. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0e4c9865..88a07d52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,8 @@ dev = [ "furo==2024.8.6", "myst-parser>=0.18", "nbsphinx>=0.9.0", + "sphinx-autodoc-typehints>=1.24.0", + "sphinx-autosummary-accessors>=2023.4.0", ] test = [ "pytest", From 9ad8672b2219622ba7395574f63a328debf906d9 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sun, 6 Jul 2025 19:43:58 +1000 Subject: [PATCH 126/241] Custom. --- docs/_static/css/custom.css | 5 +++++ docs/conf.py | 3 +++ docs/index.rst | 2 +- docs/installation.rst | 18 ------------------ 4 files changed, 9 insertions(+), 19 deletions(-) create mode 100644 docs/_static/css/custom.css diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 00000000..6beb551f --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,5 @@ +/* Custom styling for stLearn documentation */ + +p img { + vertical-align: bottom +} diff --git a/docs/conf.py b/docs/conf.py index fe622465..46368fe7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,9 @@ html_theme = 'furo' html_static_path = ['_static'] +html_css_files = [ + 'css/custom.css', +] # Configure nbsphinx nbsphinx_execute = 'never' # Don't re-execute notebooks diff --git a/docs/index.rst b/docs/index.rst index c1fb1d1f..142e73d8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,6 @@ -stLearn - Spatial Transcriptomics Toolkit +stLearn - A Spatial Transcriptomics Toolkit ============================================================================ |PyPI| |PyPIDownloads| |Docs| diff --git a/docs/installation.rst b/docs/installation.rst index 4e18ad51..f46d62f8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -5,24 +5,6 @@ Installation ============ -Install by Anaconda -------------------- - -**Step 1:** - -Prepare conda environment for stLearn -:: - - conda create -n stlearn python=3.10 --y - conda activate stlearn - -**Step 2:** - -You can directly install stlearn in the anaconda by: -:: - - conda install -c conda-forge stlearn - Install by PyPi --------------- From 48f84b9679d80f1a42a177ac93619bfa35ea9b82 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sun, 6 Jul 2025 19:51:12 +1000 Subject: [PATCH 127/241] All tutorials. --- docs/tutorials.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 3c9a62cb..83889f22 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -5,14 +5,23 @@ Tutorials :caption: Main features: tutorials/cell_cell_interaction + tutorials/cell_cell_interaction_xenium + tutorials/pseudotime_space + tutorials/pseudotime_space_xenium + tutorials/stsme_clustering + tutorials/stsme_comparison + .. nbgallery:: :caption: Visualisation and additional functionalities: tutorials/core_plots + tutorials/integrate_multiple_datasets .. nbgallery:: :caption: Supporting platforms: .. nbgallery:: :caption: Integration with other spatial tools: + + tutorials/working_with_scanpy From d7e8dc8221d77a879bc10fe8db5cd50c902e578c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 06:44:06 +1000 Subject: [PATCH 128/241] Add downloading the tutorial from google drive. --- docs/conf.py | 31 ++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 46368fe7..ef382ebe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,8 +1,33 @@ import os import sys +import re +import requests + sys.path.insert(0, os.path.abspath("..")) import stlearn +def download_gdrive_file(file_id, filename): + session = requests.Session() + url = f"https://docs.google.com/uc?export=download&id={file_id}" + response = session.get(url) + + form_action_match = re.search(r'action="([^"]+)"', response.text) + if not form_action_match: + raise Exception("Could not find form action URL") + download_url = form_action_match.group(1) + + params = {} + hidden_inputs = re.findall( + r'=1.16", "pytest>=7.0", "tox>=4.0", + "ghp-import>=2.1.0", "sphinx>=4.0", "furo==2024.8.6", "myst-parser>=0.18", From 8d940e8ee3664ec54627ca6f576c8774937a844f Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 06:46:10 +1000 Subject: [PATCH 129/241] Upgrade. --- .readthedocs.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 3ee3a06a..5552fdfd 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,4 +1,23 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need build: - image: latest -python: - version: 3.10 + os: ubuntu-24.04 + tools: + python: "3.10" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally, but recommended, +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt + \ No newline at end of file From f046d88542a52f8a4a46a2c4517632b096453d5d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 06:50:50 +1000 Subject: [PATCH 130/241] Fix import issues. --- docs/conf.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index ef382ebe..31b727d3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -79,4 +79,11 @@ def setup(app): autosummary_imported_members = True # Output directory for autosummary -autosummary_generate_overwrite = True \ No newline at end of file +autosummary_generate_overwrite = True + +autodoc_mock_imports = [ + 'numpy', 'pandas', 'scipy', 'sklearn', 'scanpy', 'anndata', + 'matplotlib', 'seaborn', 'plotly', 'bokeh', 'cv2', 'PIL', + 'rpy2', 'louvain', 'numba', 'leidenalg', + # Add any other packages causing import issues +] \ No newline at end of file From 203c9e567419ece4e1dff17a330f959c58cc99de Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 06:53:55 +1000 Subject: [PATCH 131/241] Fix import issues. --- docs/conf.py | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 31b727d3..984a5292 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,9 +3,6 @@ import re import requests -sys.path.insert(0, os.path.abspath("..")) -import stlearn - def download_gdrive_file(file_id, filename): session = requests.Session() url = f"https://docs.google.com/uc?export=download&id={file_id}" @@ -39,7 +36,6 @@ def download_gdrive_file(file_id, filename): project = 'stLearn' copyright = '2022-2025, Genomics and Machine Learning Lab' author = 'Genomics and Machine Learning Lab' -release = stlearn.__version__ html_logo = "images/logo.png" # -- General configuration --------------------------------------------------- @@ -82,8 +78,41 @@ def setup(app): autosummary_generate_overwrite = True autodoc_mock_imports = [ - 'numpy', 'pandas', 'scipy', 'sklearn', 'scanpy', 'anndata', - 'matplotlib', 'seaborn', 'plotly', 'bokeh', 'cv2', 'PIL', - 'rpy2', 'louvain', 'numba', 'leidenalg', - # Add any other packages causing import issues + 'numpy', + 'pandas', + 'scipy', + 'sklearn', + 'scanpy', + 'anndata', + 'matplotlib', + 'seaborn', + 'plotly', + 'bokeh', + 'cv2', + 'PIL', + 'rpy2', + 'louvain', + 'numba', + 'leidenalg', + 'squidpy', + 'cellphonedb', + 'torch', + 'tensorflow', + 'keras', + 'networkx', + 'igraph', + 'fa2', + 'umap', + 'phate', + 'harmonypy', + 'bbknn', + 'scanorama', + 'combat', + 'magic', + 'palantir', + 'pypng', + 'tifffile', + 'imageio', + 'skimage', + 'cv2', ] \ No newline at end of file From 07fd1965399ecea8d2fcbba9681e419fce24d89a Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 06:55:57 +1000 Subject: [PATCH 132/241] Try. --- docs/requirements.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..51f463c1 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,13 @@ +# Documentation dependencies +sphinx>=4.0 +furo==2024.8.6 +myst-parser>=0.18 +nbsphinx>=0.9.0 +sphinx-autodoc-typehints>=1.24.0 +sphinx-autosummary-accessors>=2023.4.0 +sphinx-copybutton>=0.5.2 +ipykernel>=6.0.0 + +# Core dependencies (lightweight versions) +numpy +pandas \ No newline at end of file From 9f6c048ac9a590cf8df97bec52d5303969daba4c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 06:57:17 +1000 Subject: [PATCH 133/241] Forgot. --- .readthedocs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 5552fdfd..cb6c38a7 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -17,7 +17,7 @@ sphinx: # Optionally, but recommended, # declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt + python: + install: + - requirements: docs/requirements.txt \ No newline at end of file From f7a2dcc31eec7e2e4f03917bab89590f93c17c6b Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 06:57:39 +1000 Subject: [PATCH 134/241] Forgot. --- .readthedocs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index cb6c38a7..0bcd807b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -17,7 +17,7 @@ sphinx: # Optionally, but recommended, # declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html - python: - install: - - requirements: docs/requirements.txt +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file From 3fa76e44c12ff54473150dab46c9aa19d22396be Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 07:02:11 +1000 Subject: [PATCH 135/241] Forgot. --- .readthedocs.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 0bcd807b..5460350a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,15 +9,20 @@ build: os: ubuntu-24.04 tools: python: "3.10" + jobs: + post_create_environment: + - apt-get update + - apt-get install -y pandoc + post_install: + - pip install -e . # Build documentation in the "docs/" directory with Sphinx sphinx: - configuration: docs/conf.py + configuration: docs/conf.py # Optionally, but recommended, # declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: - install: - - requirements: docs/requirements.txt - \ No newline at end of file + install: + - requirements: docs/requirements.txt From 6b0d2492c26286c7f7be1aaa750a482ac34bee80 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 07:03:05 +1000 Subject: [PATCH 136/241] Try. --- .readthedocs.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 5460350a..2292fec4 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,10 +9,6 @@ build: os: ubuntu-24.04 tools: python: "3.10" - jobs: - post_create_environment: - - apt-get update - - apt-get install -y pandoc post_install: - pip install -e . From 84b01234100a954a6966c04473ceb733eee46565 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 07:08:05 +1000 Subject: [PATCH 137/241] Try. --- .readthedocs.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 2292fec4..217fcd3c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,8 +9,6 @@ build: os: ubuntu-24.04 tools: python: "3.10" - post_install: - - pip install -e . # Build documentation in the "docs/" directory with Sphinx sphinx: @@ -22,3 +20,7 @@ sphinx: python: install: - requirements: docs/requirements.txt + - method: pip + path: . + extra_requirements: + - dev \ No newline at end of file From b3f77ad6ae0d2fb6d63eea2020440e70c62b994c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 09:15:21 +1000 Subject: [PATCH 138/241] Try. --- docs/requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 51f463c1..0db7afd2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -8,6 +8,10 @@ sphinx-autosummary-accessors>=2023.4.0 sphinx-copybutton>=0.5.2 ipykernel>=6.0.0 +# Typing +typing-extensions>=4.0.0 +types-setuptools + # Core dependencies (lightweight versions) numpy pandas \ No newline at end of file From 2888c061ea60117bc0504d14279fa56505dff0cd Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 09:30:02 +1000 Subject: [PATCH 139/241] Just rely on project installation. --- .readthedocs.yml | 1 - docs/conf.py | 40 ---------------------------------------- docs/requirements.txt | 17 ----------------- 3 files changed, 58 deletions(-) delete mode 100644 docs/requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml index 217fcd3c..e841d344 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -19,7 +19,6 @@ sphinx: # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - - requirements: docs/requirements.txt - method: pip path: . extra_requirements: diff --git a/docs/conf.py b/docs/conf.py index 984a5292..ccda1308 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,43 +76,3 @@ def setup(app): # Output directory for autosummary autosummary_generate_overwrite = True - -autodoc_mock_imports = [ - 'numpy', - 'pandas', - 'scipy', - 'sklearn', - 'scanpy', - 'anndata', - 'matplotlib', - 'seaborn', - 'plotly', - 'bokeh', - 'cv2', - 'PIL', - 'rpy2', - 'louvain', - 'numba', - 'leidenalg', - 'squidpy', - 'cellphonedb', - 'torch', - 'tensorflow', - 'keras', - 'networkx', - 'igraph', - 'fa2', - 'umap', - 'phate', - 'harmonypy', - 'bbknn', - 'scanorama', - 'combat', - 'magic', - 'palantir', - 'pypng', - 'tifffile', - 'imageio', - 'skimage', - 'cv2', -] \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 0db7afd2..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# Documentation dependencies -sphinx>=4.0 -furo==2024.8.6 -myst-parser>=0.18 -nbsphinx>=0.9.0 -sphinx-autodoc-typehints>=1.24.0 -sphinx-autosummary-accessors>=2023.4.0 -sphinx-copybutton>=0.5.2 -ipykernel>=6.0.0 - -# Typing -typing-extensions>=4.0.0 -types-setuptools - -# Core dependencies (lightweight versions) -numpy -pandas \ No newline at end of file From df82f78e1bdbb21c7b88fd4d3c0042452ac7aec6 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 12:42:24 +1000 Subject: [PATCH 140/241] Sphinx gets confused with renaming package imports. Making more consistent. --- HISTORY.rst | 1 + docs/api.rst | 16 +--- docs/release_notes/1.1.0.rst | 3 +- stlearn/adds/add_mask.py | 2 +- stlearn/app/app.py | 10 +-- stlearn/app/source/forms/views.py | 2 +- stlearn/pl.py | 60 --------------- stlearn/{plotting => pl}/QC_plot.py | 0 stlearn/pl/__init__.py | 77 +++++++++++++++++++ stlearn/{plotting => pl}/_docs.py | 0 stlearn/{plotting => pl}/cci_plot.py | 4 +- stlearn/{plotting => pl}/cci_plot_helpers.py | 2 +- stlearn/{plotting => pl}/classes.py | 0 stlearn/{plotting => pl}/classes_bokeh.py | 8 +- stlearn/{plotting => pl}/cluster_plot.py | 6 +- .../{plotting => pl}/deconvolution_plot.py | 0 stlearn/{plotting => pl}/feat_plot.py | 2 +- stlearn/{plotting => pl}/gene_plot.py | 6 +- stlearn/{plotting => pl}/mask_plot.py | 2 +- stlearn/{plotting => pl}/non_spatial_plot.py | 0 stlearn/{plotting => pl}/palettes_st.py | 0 stlearn/{plotting => pl}/stack_3d_plot.py | 0 stlearn/{plotting => pl}/subcluster_plot.py | 4 +- .../trajectory/DE_transition_plot.py | 0 .../{plotting => pl}/trajectory/__init__.py | 10 ++- .../trajectory/check_trajectory.py | 0 .../{plotting => pl}/trajectory/local_plot.py | 0 .../trajectory/pseudotime_plot.py | 2 +- .../trajectory/transition_markers_plot.py | 0 .../{plotting => pl}/trajectory/tree_plot.py | 0 .../trajectory/tree_plot_simple.py | 0 stlearn/{plotting => pl}/trajectory/utils.py | 0 stlearn/{plotting => pl}/utils.py | 2 +- stlearn/spatial.py | 9 --- stlearn/{spatials => spatial}/SME/__init__.py | 0 .../SME/_weighting_matrix.py | 0 stlearn/{spatials => spatial}/SME/impute.py | 0 .../{spatials => spatial}/SME/normalize.py | 0 stlearn/spatial/__init__.py | 15 ++++ .../clustering/__init__.py | 0 .../clustering/localization.py | 0 .../morphology/__init__.py | 0 .../morphology/adjust.py | 0 .../{spatials => spatial}/smooth/__init__.py | 0 stlearn/{spatials => spatial}/smooth/disk.py | 0 .../trajectory/__init__.py | 0 .../trajectory/compare_transitions.py | 0 .../trajectory/detect_transition_markers.py | 0 .../trajectory/global_level.py | 0 .../trajectory/local_level.py | 0 .../trajectory/pseudotime.py | 4 +- .../trajectory/pseudotimespace.py | 0 .../trajectory/set_root.py | 2 +- .../trajectory/shortest_path_spatial_PAGA.py | 2 +- .../{spatials => spatial}/trajectory/utils.py | 0 .../trajectory/weight_optimization.py | 0 stlearn/spatials/__init__.py | 0 stlearn/tl.py | 10 --- stlearn/tl/__init__.py | 13 ++++ stlearn/{tools => tl}/cache/__init__.py | 0 stlearn/{tools => tl}/cache/anndata.py | 0 stlearn/tl/cci/__init__.py | 4 + .../{tools/microenv => tl}/cci/analysis.py | 0 stlearn/{tools/microenv => tl}/cci/base.py | 0 .../microenv => tl}/cci/base_grouping.py | 0 .../cci/databases}/__init__.py | 0 .../cci/databases/connectomeDB2020_lit.txt | 0 .../cci/databases/connectomeDB2020_put.txt | 0 stlearn/{tools/microenv => tl}/cci/go.R | 0 stlearn/{tools/microenv => tl}/cci/go.py | 2 +- stlearn/{tools/microenv => tl}/cci/het.py | 2 +- .../{tools/microenv => tl}/cci/het_helpers.py | 0 stlearn/{tools/microenv => tl}/cci/merge.py | 0 .../{tools/microenv => tl}/cci/perm_utils.py | 0 .../{tools/microenv => tl}/cci/permutation.py | 0 .../{tools/microenv => tl}/cci/r_helpers.py | 0 stlearn/{tools => tl}/clustering/__init__.py | 0 stlearn/{tools => tl}/clustering/annotate.py | 2 +- stlearn/{tools => tl}/clustering/kmeans.py | 29 +++---- stlearn/{tools => tl}/clustering/louvain.py | 0 stlearn/tl/label/__init__.py | 7 ++ stlearn/{tools => tl}/label/label.py | 2 +- stlearn/{tools => tl}/label/label_transfer.R | 0 stlearn/{tools => tl}/label/rctd.R | 0 stlearn/{tools => tl}/label/singleR.R | 0 stlearn/tools/__init__.py | 0 stlearn/tools/label/__init__.py | 0 stlearn/tools/microenv/__init__.py | 0 stlearn/tools/microenv/cci/__init__.py | 16 ---- .../tools/microenv/cci/databases/__init__.py | 0 stlearn/wrapper/read.py | 3 - tests/test_CCI.py | 4 +- tests/test_cluster_plot.py | 2 +- 93 files changed, 179 insertions(+), 168 deletions(-) delete mode 100644 stlearn/pl.py rename stlearn/{plotting => pl}/QC_plot.py (100%) create mode 100644 stlearn/pl/__init__.py rename stlearn/{plotting => pl}/_docs.py (100%) rename stlearn/{plotting => pl}/cci_plot.py (99%) rename stlearn/{plotting => pl}/cci_plot_helpers.py (99%) rename stlearn/{plotting => pl}/classes.py (100%) rename stlearn/{plotting => pl}/classes_bokeh.py (99%) rename stlearn/{plotting => pl}/cluster_plot.py (95%) rename stlearn/{plotting => pl}/deconvolution_plot.py (100%) rename stlearn/{plotting => pl}/feat_plot.py (97%) rename stlearn/{plotting => pl}/gene_plot.py (93%) rename stlearn/{plotting => pl}/mask_plot.py (99%) rename stlearn/{plotting => pl}/non_spatial_plot.py (100%) rename stlearn/{plotting => pl}/palettes_st.py (100%) rename stlearn/{plotting => pl}/stack_3d_plot.py (100%) rename stlearn/{plotting => pl}/subcluster_plot.py (94%) rename stlearn/{plotting => pl}/trajectory/DE_transition_plot.py (100%) rename stlearn/{plotting => pl}/trajectory/__init__.py (93%) rename stlearn/{plotting => pl}/trajectory/check_trajectory.py (100%) rename stlearn/{plotting => pl}/trajectory/local_plot.py (100%) rename stlearn/{plotting => pl}/trajectory/pseudotime_plot.py (99%) rename stlearn/{plotting => pl}/trajectory/transition_markers_plot.py (100%) rename stlearn/{plotting => pl}/trajectory/tree_plot.py (100%) rename stlearn/{plotting => pl}/trajectory/tree_plot_simple.py (100%) rename stlearn/{plotting => pl}/trajectory/utils.py (100%) rename stlearn/{plotting => pl}/utils.py (98%) delete mode 100644 stlearn/spatial.py rename stlearn/{spatials => spatial}/SME/__init__.py (100%) rename stlearn/{spatials => spatial}/SME/_weighting_matrix.py (100%) rename stlearn/{spatials => spatial}/SME/impute.py (100%) rename stlearn/{spatials => spatial}/SME/normalize.py (100%) create mode 100644 stlearn/spatial/__init__.py rename stlearn/{spatials => spatial}/clustering/__init__.py (100%) rename stlearn/{spatials => spatial}/clustering/localization.py (100%) rename stlearn/{spatials => spatial}/morphology/__init__.py (100%) rename stlearn/{spatials => spatial}/morphology/adjust.py (100%) rename stlearn/{spatials => spatial}/smooth/__init__.py (100%) rename stlearn/{spatials => spatial}/smooth/disk.py (100%) rename stlearn/{spatials => spatial}/trajectory/__init__.py (100%) rename stlearn/{spatials => spatial}/trajectory/compare_transitions.py (100%) rename stlearn/{spatials => spatial}/trajectory/detect_transition_markers.py (100%) rename stlearn/{spatials => spatial}/trajectory/global_level.py (100%) rename stlearn/{spatials => spatial}/trajectory/local_level.py (100%) rename stlearn/{spatials => spatial}/trajectory/pseudotime.py (98%) rename stlearn/{spatials => spatial}/trajectory/pseudotimespace.py (100%) rename stlearn/{spatials => spatial}/trajectory/set_root.py (97%) rename stlearn/{spatials => spatial}/trajectory/shortest_path_spatial_PAGA.py (98%) rename stlearn/{spatials => spatial}/trajectory/utils.py (100%) rename stlearn/{spatials => spatial}/trajectory/weight_optimization.py (100%) delete mode 100644 stlearn/spatials/__init__.py delete mode 100644 stlearn/tl.py create mode 100644 stlearn/tl/__init__.py rename stlearn/{tools => tl}/cache/__init__.py (100%) rename stlearn/{tools => tl}/cache/anndata.py (100%) create mode 100644 stlearn/tl/cci/__init__.py rename stlearn/{tools/microenv => tl}/cci/analysis.py (100%) rename stlearn/{tools/microenv => tl}/cci/base.py (100%) rename stlearn/{tools/microenv => tl}/cci/base_grouping.py (100%) rename stlearn/{plotting => tl/cci/databases}/__init__.py (100%) rename stlearn/{tools/microenv => tl}/cci/databases/connectomeDB2020_lit.txt (100%) rename stlearn/{tools/microenv => tl}/cci/databases/connectomeDB2020_put.txt (100%) rename stlearn/{tools/microenv => tl}/cci/go.R (100%) rename stlearn/{tools/microenv => tl}/cci/go.py (95%) rename stlearn/{tools/microenv => tl}/cci/het.py (99%) rename stlearn/{tools/microenv => tl}/cci/het_helpers.py (100%) rename stlearn/{tools/microenv => tl}/cci/merge.py (100%) rename stlearn/{tools/microenv => tl}/cci/perm_utils.py (100%) rename stlearn/{tools/microenv => tl}/cci/permutation.py (100%) rename stlearn/{tools/microenv => tl}/cci/r_helpers.py (100%) rename stlearn/{tools => tl}/clustering/__init__.py (100%) rename stlearn/{tools => tl}/clustering/annotate.py (88%) rename stlearn/{tools => tl}/clustering/kmeans.py (79%) rename stlearn/{tools => tl}/clustering/louvain.py (100%) create mode 100644 stlearn/tl/label/__init__.py rename stlearn/{tools => tl}/label/label.py (99%) rename stlearn/{tools => tl}/label/label_transfer.R (100%) rename stlearn/{tools => tl}/label/rctd.R (100%) rename stlearn/{tools => tl}/label/singleR.R (100%) delete mode 100644 stlearn/tools/__init__.py delete mode 100644 stlearn/tools/label/__init__.py delete mode 100644 stlearn/tools/microenv/__init__.py delete mode 100644 stlearn/tools/microenv/cci/__init__.py delete mode 100644 stlearn/tools/microenv/cci/databases/__init__.py diff --git a/HISTORY.rst b/HISTORY.rst index 8c608fe3..2714496b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,7 @@ API and Bug Fixes: * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. * Removed datasets.example_bcba - Replaced with wrapper for scanpy.visium_sge. +* Moved spatials directory to spatial. 0.4.11 (2022-11-25) ------------------ diff --git a/docs/api.rst b/docs/api.rst index 104ad279..c27132ff 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -13,7 +13,6 @@ Import stLearn as:: Wrapper functions: `wrapper` ------------------------------ -.. module:: stlearn.wrapper .. currentmodule:: stlearn .. autosummary:: @@ -31,7 +30,6 @@ Wrapper functions: `wrapper` Add: `add` ------------------- -.. module:: stlearn.add .. currentmodule:: stlearn .. autosummary:: @@ -130,7 +128,6 @@ Spatial: `spatial` Tools: `tl` ------------------- -.. module:: stlearn.tl.clustering .. currentmodule:: stlearn .. autosummary:: @@ -138,13 +135,6 @@ Tools: `tl` tl.clustering.kmeans tl.clustering.louvain - -.. module:: stlearn.tl.cci -.. currentmodule:: stlearn - -.. autosummary:: - :toctree: api/ - tl.cci.load_lrs tl.cci.grid tl.cci.run @@ -155,7 +145,6 @@ Tools: `tl` Plot: `pl` ------------------- -.. module:: stlearn.pl .. currentmodule:: stlearn .. autosummary:: @@ -167,7 +156,6 @@ Plot: `pl` pl.cluster_plot pl.cluster_plot_interactive pl.subcluster_plot - pl.subcluster_plot pl.non_spatial_plot pl.deconvolution_plot pl.plot_mask @@ -185,7 +173,6 @@ Plot: `pl` pl.lr_plot_interactive pl.spatialcci_plot_interactive -.. module:: stlearn.pl.trajectory .. currentmodule:: stlearn .. autosummary:: @@ -198,9 +185,8 @@ Plot: `pl` pl.trajectory.DE_transition_plot Datasets: `datasets` -------------------- +--------------------------- -.. module:: stlearn.datasets .. currentmodule:: stlearn .. autosummary:: diff --git a/docs/release_notes/1.1.0.rst b/docs/release_notes/1.1.0.rst index 9d040781..e255fdd0 100644 --- a/docs/release_notes/1.1.0.rst +++ b/docs/release_notes/1.1.0.rst @@ -15,4 +15,5 @@ * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. -* Removed datasets.example_bcba - Replaced with wrapper for scanpy.visium_sge. \ No newline at end of file +* Removed datasets.example_bcba - Replaced with wrapper for scanpy.visium_sge. +* Moved spatials directory to spatial. \ No newline at end of file diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index 998b4936..d25a488c 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -106,7 +106,7 @@ def apply_mask( """ from scanpy.plotting import palettes - from stlearn.plotting import palettes_st + from stlearn.pl import palettes_st adata = adata.copy() if copy else adata diff --git a/stlearn/app/app.py b/stlearn/app/app.py index 25964ae7..d1e914f3 100644 --- a/stlearn/app/app.py +++ b/stlearn/app/app.py @@ -366,7 +366,7 @@ def save_adata(): def modify_doc_gene_plot(doc): - from stlearn.plotting.classes_bokeh import BokehGenePlot + from stlearn.pl.classes_bokeh import BokehGenePlot gp_object = BokehGenePlot(adata) doc.add_root(row(gp_object.layout, width=800)) @@ -383,7 +383,7 @@ def modify_doc_gene_plot(doc): def modify_doc_cluster_plot(doc): - from stlearn.plotting.classes_bokeh import BokehClusterPlot + from stlearn.pl.classes_bokeh import BokehClusterPlot gp_object = BokehClusterPlot(adata) doc.add_root(row(gp_object.layout, width=800)) @@ -404,7 +404,7 @@ def modify_doc_cluster_plot(doc): def modify_doc_spatial_cci_plot(doc): - from stlearn.plotting.classes_bokeh import BokehSpatialCciPlot + from stlearn.pl.classes_bokeh import BokehSpatialCciPlot gp_object = BokehSpatialCciPlot(adata) doc.add_root(row(gp_object.layout, width=800)) @@ -420,7 +420,7 @@ def modify_doc_spatial_cci_plot(doc): def modify_doc_lr_plot(doc): - from stlearn.plotting.classes_bokeh import BokehLRPlot + from stlearn.pl.classes_bokeh import BokehLRPlot gp_object = BokehLRPlot(adata) doc.add_root(row(gp_object.layout, width=800)) @@ -434,7 +434,7 @@ def modify_doc_lr_plot(doc): def modify_doc_annotate_plot(doc): - from stlearn.plotting.classes_bokeh import Annotate + from stlearn.pl.classes_bokeh import Annotate gp_object = Annotate(adata) doc.add_root(row(gp_object.layout, width=800)) diff --git a/stlearn/app/source/forms/views.py b/stlearn/app/source/forms/views.py index 3aa58bbb..3a857319 100644 --- a/stlearn/app/source/forms/views.py +++ b/stlearn/app/source/forms/views.py @@ -286,7 +286,7 @@ def run_psts(request, adata, step_log): else: try: - from stlearn.spatials.trajectory import set_root + from stlearn.spatial.trajectory import set_root root_index = set_root( adata, use_label="clusters", cluster=str(element_values[0]) diff --git a/stlearn/pl.py b/stlearn/pl.py deleted file mode 100644 index 78db0c8a..00000000 --- a/stlearn/pl.py +++ /dev/null @@ -1,60 +0,0 @@ -# from .plotting.cci_plot import het_plot_interactive -from .plotting import trajectory - -# from .plotting.cci_plot import het_plot_interactive -from .plotting.cci_plot import ( - cci_check, - cci_map, - ccinet_plot, - grid_plot, - het_plot, - lr_cci_map, - lr_chord_plot, - lr_diagnostics, - lr_go, - lr_n_spots, - lr_plot, - lr_plot_interactive, - lr_result_plot, - lr_summary, - spatialcci_plot_interactive, -) -from .plotting.cluster_plot import cluster_plot, cluster_plot_interactive -from .plotting.deconvolution_plot import deconvolution_plot -from .plotting.feat_plot import feat_plot -from .plotting.gene_plot import gene_plot, gene_plot_interactive -from .plotting.mask_plot import plot_mask -from .plotting.non_spatial_plot import non_spatial_plot -from .plotting.QC_plot import QC_plot -from .plotting.stack_3d_plot import stack_3d_plot -from .plotting.subcluster_plot import subcluster_plot - -__all__ = [ - "gene_plot", - "gene_plot_interactive", - "feat_plot", - "cluster_plot", - "cluster_plot_interactive", - "subcluster_plot", - "non_spatial_plot", - "deconvolution_plot", - "stack_3d_plot", - "trajectory", - "QC_plot", - "het_plot", - "lr_plot_interactive", - "spatialcci_plot_interactive", - "grid_plot", - "lr_diagnostics", - "lr_n_spots", - "lr_summary", - "lr_go", - "lr_plot", - "lr_result_plot", - "ccinet_plot", - "cci_map", - "lr_cci_map", - "lr_chord_plot", - "cci_check", - "plot_mask", -] diff --git a/stlearn/plotting/QC_plot.py b/stlearn/pl/QC_plot.py similarity index 100% rename from stlearn/plotting/QC_plot.py rename to stlearn/pl/QC_plot.py diff --git a/stlearn/pl/__init__.py b/stlearn/pl/__init__.py new file mode 100644 index 00000000..c24e63a2 --- /dev/null +++ b/stlearn/pl/__init__.py @@ -0,0 +1,77 @@ +# Import individual functions from modules +from .cci_plot import ( + cci_check, + cci_map, + ccinet_plot, + grid_plot, + het_plot, + lr_cci_map, + lr_chord_plot, + lr_diagnostics, + lr_go, + lr_n_spots, + lr_plot, + lr_plot_interactive, + lr_result_plot, + lr_summary, + spatialcci_plot_interactive, +) +from .cluster_plot import cluster_plot, cluster_plot_interactive +from .deconvolution_plot import deconvolution_plot +from .feat_plot import feat_plot +from .gene_plot import gene_plot, gene_plot_interactive +from .mask_plot import plot_mask +from .non_spatial_plot import non_spatial_plot +from .QC_plot import QC_plot +from .stack_3d_plot import stack_3d_plot +from .subcluster_plot import subcluster_plot + +# Import trajectory functions +from .trajectory import ( + DE_transition_plot, + check_trajectory, + local_plot, + pseudotime_plot, + transition_markers_plot, + tree_plot, + tree_plot_simple, +) + +__all__ = [ + # CCI plot functions + "cci_check", + "cci_map", + "ccinet_plot", + "grid_plot", + "het_plot", + "lr_cci_map", + "lr_chord_plot", + "lr_diagnostics", + "lr_go", + "lr_n_spots", + "lr_plot", + "lr_plot_interactive", + "lr_result_plot", + "lr_summary", + "spatialcci_plot_interactive", + # Other plot functions + "cluster_plot", + "cluster_plot_interactive", + "deconvolution_plot", + "feat_plot", + "gene_plot", + "gene_plot_interactive", + "plot_mask", + "non_spatial_plot", + "QC_plot", + "stack_3d_plot", + "subcluster_plot", + # Trajectory functions + "pseudotime_plot", + "local_plot", + "tree_plot", + "transition_markers_plot", + "DE_transition_plot", + "tree_plot_simple", + "check_trajectory", +] diff --git a/stlearn/plotting/_docs.py b/stlearn/pl/_docs.py similarity index 100% rename from stlearn/plotting/_docs.py rename to stlearn/pl/_docs.py diff --git a/stlearn/plotting/cci_plot.py b/stlearn/pl/cci_plot.py similarity index 99% rename from stlearn/plotting/cci_plot.py rename to stlearn/pl/cci_plot.py index 95fde072..cc8e0918 100644 --- a/stlearn/plotting/cci_plot.py +++ b/stlearn/pl/cci_plot.py @@ -19,8 +19,8 @@ from bokeh.plotting import show from scipy.stats import gaussian_kde -import stlearn.plotting.cci_plot_helpers as cci_hs -from stlearn.plotting.utils import get_colors +import stlearn.pl.cci_plot_helpers as cci_hs +from stlearn.pl.utils import get_colors from ..utils import _docs_params from ._docs import doc_het_plot, doc_spatial_base_plot diff --git a/stlearn/plotting/cci_plot_helpers.py b/stlearn/pl/cci_plot_helpers.py similarity index 99% rename from stlearn/plotting/cci_plot_helpers.py rename to stlearn/pl/cci_plot_helpers.py index 25b2a25e..28ac1b5a 100644 --- a/stlearn/plotting/cci_plot_helpers.py +++ b/stlearn/pl/cci_plot_helpers.py @@ -13,7 +13,7 @@ from matplotlib.path import Path from mpl_toolkits.axes_grid1 import make_axes_locatable -from ..tools.microenv.cci.het import get_edges +from stlearn.tl.cci.het import get_edges # Helper functions for overview plots of the LRs. diff --git a/stlearn/plotting/classes.py b/stlearn/pl/classes.py similarity index 100% rename from stlearn/plotting/classes.py rename to stlearn/pl/classes.py diff --git a/stlearn/plotting/classes_bokeh.py b/stlearn/pl/classes_bokeh.py similarity index 99% rename from stlearn/plotting/classes_bokeh.py rename to stlearn/pl/classes_bokeh.py index 54a69c0b..cd52d16a 100644 --- a/stlearn/plotting/classes_bokeh.py +++ b/stlearn/pl/classes_bokeh.py @@ -43,7 +43,7 @@ from PIL import Image from stlearn.classes import Spatial -from stlearn.tools.microenv.cci.het import get_edges +from stlearn.tl.cci import get_edges from stlearn.utils import _read_graph @@ -357,7 +357,7 @@ def __init__( self.use_label = Select(title="Select use_label:", value=menu[0], options=menu) # Initialize the color - from stlearn.plotting.cluster_plot import cluster_plot + from stlearn.pl.cluster_plot import cluster_plot if len(adata.obs[self.use_label.value].cat.categories) <= 20: cluster_plot(adata, use_label=self.use_label.value, show_plot=False) @@ -505,7 +505,7 @@ def modify_fig(doc): def update_list(self, attrname, old, name): # Initialize the color - from stlearn.plotting.cluster_plot import cluster_plot + from stlearn.pl.cluster_plot import cluster_plot cluster_plot(self.adata[0], use_label=self.use_label.value, show_plot=False) self.list_cluster.labels = list( @@ -1214,7 +1214,7 @@ def _add_edges(fig, adata, edges, arrow_size, forward=True, scale_factor=1): def update_list(self, attrname, old, name): # Initialize the color - from stlearn.plotting.cluster_plot import cluster_plot + from stlearn.pl.cluster_plot import cluster_plot selected = self.annot_select.value.strip("raw_") cluster_plot(self.adata[0], use_label=selected, show_plot=False) diff --git a/stlearn/plotting/cluster_plot.py b/stlearn/pl/cluster_plot.py similarity index 95% rename from stlearn/plotting/cluster_plot.py rename to stlearn/pl/cluster_plot.py index 1293dab6..288f0b7f 100644 --- a/stlearn/plotting/cluster_plot.py +++ b/stlearn/pl/cluster_plot.py @@ -7,9 +7,9 @@ from bokeh.io import output_notebook from bokeh.plotting import show -from stlearn.plotting._docs import doc_cluster_plot, doc_spatial_base_plot -from stlearn.plotting.classes import ClusterPlot -from stlearn.plotting.classes_bokeh import BokehClusterPlot +from stlearn.pl._docs import doc_cluster_plot, doc_spatial_base_plot +from stlearn.pl.classes import ClusterPlot +from stlearn.pl.classes_bokeh import BokehClusterPlot from stlearn.utils import _docs_params diff --git a/stlearn/plotting/deconvolution_plot.py b/stlearn/pl/deconvolution_plot.py similarity index 100% rename from stlearn/plotting/deconvolution_plot.py rename to stlearn/pl/deconvolution_plot.py diff --git a/stlearn/plotting/feat_plot.py b/stlearn/pl/feat_plot.py similarity index 97% rename from stlearn/plotting/feat_plot.py rename to stlearn/pl/feat_plot.py index 7a3d4bf7..26430cbb 100644 --- a/stlearn/plotting/feat_plot.py +++ b/stlearn/pl/feat_plot.py @@ -9,7 +9,7 @@ import matplotlib from anndata import AnnData -from stlearn.plotting.classes import FeaturePlot +from stlearn.pl.classes import FeaturePlot # @_docs_params(spatial_base_plot=doc_spatial_base_plot, gene_plot=doc_gene_plot) diff --git a/stlearn/plotting/gene_plot.py b/stlearn/pl/gene_plot.py similarity index 93% rename from stlearn/plotting/gene_plot.py rename to stlearn/pl/gene_plot.py index 8f4244c8..5b545170 100644 --- a/stlearn/plotting/gene_plot.py +++ b/stlearn/pl/gene_plot.py @@ -3,9 +3,9 @@ from bokeh.io import output_notebook from bokeh.plotting import show -from stlearn.plotting._docs import doc_gene_plot, doc_spatial_base_plot -from stlearn.plotting.classes import GenePlot -from stlearn.plotting.classes_bokeh import BokehGenePlot +from stlearn.pl._docs import doc_gene_plot, doc_spatial_base_plot +from stlearn.pl.classes import GenePlot +from stlearn.pl.classes_bokeh import BokehGenePlot from stlearn.utils import _docs_params diff --git a/stlearn/plotting/mask_plot.py b/stlearn/pl/mask_plot.py similarity index 99% rename from stlearn/plotting/mask_plot.py rename to stlearn/pl/mask_plot.py index 05ee3f9f..60762ccc 100644 --- a/stlearn/plotting/mask_plot.py +++ b/stlearn/pl/mask_plot.py @@ -58,7 +58,7 @@ def plot_mask( """ from scanpy.plotting import palettes - from stlearn.plotting import palettes_st + from stlearn.pl import palettes_st if cmap_name == "vega_10_scanpy": cmap = palettes.vega_10_scanpy diff --git a/stlearn/plotting/non_spatial_plot.py b/stlearn/pl/non_spatial_plot.py similarity index 100% rename from stlearn/plotting/non_spatial_plot.py rename to stlearn/pl/non_spatial_plot.py diff --git a/stlearn/plotting/palettes_st.py b/stlearn/pl/palettes_st.py similarity index 100% rename from stlearn/plotting/palettes_st.py rename to stlearn/pl/palettes_st.py diff --git a/stlearn/plotting/stack_3d_plot.py b/stlearn/pl/stack_3d_plot.py similarity index 100% rename from stlearn/plotting/stack_3d_plot.py rename to stlearn/pl/stack_3d_plot.py diff --git a/stlearn/plotting/subcluster_plot.py b/stlearn/pl/subcluster_plot.py similarity index 94% rename from stlearn/plotting/subcluster_plot.py rename to stlearn/pl/subcluster_plot.py index 3f5eff3d..2b7b0ec2 100644 --- a/stlearn/plotting/subcluster_plot.py +++ b/stlearn/pl/subcluster_plot.py @@ -4,8 +4,8 @@ from anndata import AnnData -from stlearn.plotting._docs import doc_spatial_base_plot, doc_subcluster_plot -from stlearn.plotting.classes import SubClusterPlot +from stlearn.pl._docs import doc_spatial_base_plot, doc_subcluster_plot +from stlearn.pl.classes import SubClusterPlot from stlearn.utils import _AxesSubplot, _docs_params diff --git a/stlearn/plotting/trajectory/DE_transition_plot.py b/stlearn/pl/trajectory/DE_transition_plot.py similarity index 100% rename from stlearn/plotting/trajectory/DE_transition_plot.py rename to stlearn/pl/trajectory/DE_transition_plot.py diff --git a/stlearn/plotting/trajectory/__init__.py b/stlearn/pl/trajectory/__init__.py similarity index 93% rename from stlearn/plotting/trajectory/__init__.py rename to stlearn/pl/trajectory/__init__.py index 4d5d7457..2b5417d5 100644 --- a/stlearn/plotting/trajectory/__init__.py +++ b/stlearn/pl/trajectory/__init__.py @@ -1,3 +1,5 @@ +# stlearn/pl/trajectory/__init__.py + from .check_trajectory import check_trajectory from .DE_transition_plot import DE_transition_plot from .local_plot import local_plot @@ -7,11 +9,11 @@ from .tree_plot_simple import tree_plot_simple __all__ = [ - "DE_transition_plot", - "check_trajectory", - "local_plot", "pseudotime_plot", - "transition_markers_plot", + "local_plot", "tree_plot", + "transition_markers_plot", + "DE_transition_plot", "tree_plot_simple", + "check_trajectory", ] diff --git a/stlearn/plotting/trajectory/check_trajectory.py b/stlearn/pl/trajectory/check_trajectory.py similarity index 100% rename from stlearn/plotting/trajectory/check_trajectory.py rename to stlearn/pl/trajectory/check_trajectory.py diff --git a/stlearn/plotting/trajectory/local_plot.py b/stlearn/pl/trajectory/local_plot.py similarity index 100% rename from stlearn/plotting/trajectory/local_plot.py rename to stlearn/pl/trajectory/local_plot.py diff --git a/stlearn/plotting/trajectory/pseudotime_plot.py b/stlearn/pl/trajectory/pseudotime_plot.py similarity index 99% rename from stlearn/plotting/trajectory/pseudotime_plot.py rename to stlearn/pl/trajectory/pseudotime_plot.py index ee72a426..63868009 100644 --- a/stlearn/plotting/trajectory/pseudotime_plot.py +++ b/stlearn/pl/trajectory/pseudotime_plot.py @@ -5,7 +5,7 @@ from matplotlib import pyplot as plt from numpy._typing import NDArray -from stlearn.plotting.utils import get_cluster, get_node +from stlearn.pl.utils import get_cluster, get_node from stlearn.utils import _read_graph diff --git a/stlearn/plotting/trajectory/transition_markers_plot.py b/stlearn/pl/trajectory/transition_markers_plot.py similarity index 100% rename from stlearn/plotting/trajectory/transition_markers_plot.py rename to stlearn/pl/trajectory/transition_markers_plot.py diff --git a/stlearn/plotting/trajectory/tree_plot.py b/stlearn/pl/trajectory/tree_plot.py similarity index 100% rename from stlearn/plotting/trajectory/tree_plot.py rename to stlearn/pl/trajectory/tree_plot.py diff --git a/stlearn/plotting/trajectory/tree_plot_simple.py b/stlearn/pl/trajectory/tree_plot_simple.py similarity index 100% rename from stlearn/plotting/trajectory/tree_plot_simple.py rename to stlearn/pl/trajectory/tree_plot_simple.py diff --git a/stlearn/plotting/trajectory/utils.py b/stlearn/pl/trajectory/utils.py similarity index 100% rename from stlearn/plotting/trajectory/utils.py rename to stlearn/pl/trajectory/utils.py diff --git a/stlearn/plotting/utils.py b/stlearn/pl/utils.py similarity index 98% rename from stlearn/plotting/utils.py rename to stlearn/pl/utils.py index a1380899..30363049 100644 --- a/stlearn/plotting/utils.py +++ b/stlearn/pl/utils.py @@ -6,7 +6,7 @@ from PIL import Image from scanpy.plotting import palettes -from stlearn.plotting import palettes_st +from stlearn.pl import palettes_st def get_img_from_fig(fig, dpi=180): diff --git a/stlearn/spatial.py b/stlearn/spatial.py deleted file mode 100644 index cbf7eced..00000000 --- a/stlearn/spatial.py +++ /dev/null @@ -1,9 +0,0 @@ -from .spatials import SME, clustering, morphology, smooth, trajectory - -__all__ = [ - "clustering", - "smooth", - "trajectory", - "morphology", - "SME", -] diff --git a/stlearn/spatials/SME/__init__.py b/stlearn/spatial/SME/__init__.py similarity index 100% rename from stlearn/spatials/SME/__init__.py rename to stlearn/spatial/SME/__init__.py diff --git a/stlearn/spatials/SME/_weighting_matrix.py b/stlearn/spatial/SME/_weighting_matrix.py similarity index 100% rename from stlearn/spatials/SME/_weighting_matrix.py rename to stlearn/spatial/SME/_weighting_matrix.py diff --git a/stlearn/spatials/SME/impute.py b/stlearn/spatial/SME/impute.py similarity index 100% rename from stlearn/spatials/SME/impute.py rename to stlearn/spatial/SME/impute.py diff --git a/stlearn/spatials/SME/normalize.py b/stlearn/spatial/SME/normalize.py similarity index 100% rename from stlearn/spatials/SME/normalize.py rename to stlearn/spatial/SME/normalize.py diff --git a/stlearn/spatial/__init__.py b/stlearn/spatial/__init__.py new file mode 100644 index 00000000..017501be --- /dev/null +++ b/stlearn/spatial/__init__.py @@ -0,0 +1,15 @@ +# stlearn/spatial/__init__.py + +from . import SME +from . import clustering +from . import morphology +from . import smooth +from . import trajectory + +__all__ = [ + "clustering", + "smooth", + "trajectory", + "morphology", + "SME", +] \ No newline at end of file diff --git a/stlearn/spatials/clustering/__init__.py b/stlearn/spatial/clustering/__init__.py similarity index 100% rename from stlearn/spatials/clustering/__init__.py rename to stlearn/spatial/clustering/__init__.py diff --git a/stlearn/spatials/clustering/localization.py b/stlearn/spatial/clustering/localization.py similarity index 100% rename from stlearn/spatials/clustering/localization.py rename to stlearn/spatial/clustering/localization.py diff --git a/stlearn/spatials/morphology/__init__.py b/stlearn/spatial/morphology/__init__.py similarity index 100% rename from stlearn/spatials/morphology/__init__.py rename to stlearn/spatial/morphology/__init__.py diff --git a/stlearn/spatials/morphology/adjust.py b/stlearn/spatial/morphology/adjust.py similarity index 100% rename from stlearn/spatials/morphology/adjust.py rename to stlearn/spatial/morphology/adjust.py diff --git a/stlearn/spatials/smooth/__init__.py b/stlearn/spatial/smooth/__init__.py similarity index 100% rename from stlearn/spatials/smooth/__init__.py rename to stlearn/spatial/smooth/__init__.py diff --git a/stlearn/spatials/smooth/disk.py b/stlearn/spatial/smooth/disk.py similarity index 100% rename from stlearn/spatials/smooth/disk.py rename to stlearn/spatial/smooth/disk.py diff --git a/stlearn/spatials/trajectory/__init__.py b/stlearn/spatial/trajectory/__init__.py similarity index 100% rename from stlearn/spatials/trajectory/__init__.py rename to stlearn/spatial/trajectory/__init__.py diff --git a/stlearn/spatials/trajectory/compare_transitions.py b/stlearn/spatial/trajectory/compare_transitions.py similarity index 100% rename from stlearn/spatials/trajectory/compare_transitions.py rename to stlearn/spatial/trajectory/compare_transitions.py diff --git a/stlearn/spatials/trajectory/detect_transition_markers.py b/stlearn/spatial/trajectory/detect_transition_markers.py similarity index 100% rename from stlearn/spatials/trajectory/detect_transition_markers.py rename to stlearn/spatial/trajectory/detect_transition_markers.py diff --git a/stlearn/spatials/trajectory/global_level.py b/stlearn/spatial/trajectory/global_level.py similarity index 100% rename from stlearn/spatials/trajectory/global_level.py rename to stlearn/spatial/trajectory/global_level.py diff --git a/stlearn/spatials/trajectory/local_level.py b/stlearn/spatial/trajectory/local_level.py similarity index 100% rename from stlearn/spatials/trajectory/local_level.py rename to stlearn/spatial/trajectory/local_level.py diff --git a/stlearn/spatials/trajectory/pseudotime.py b/stlearn/spatial/trajectory/pseudotime.py similarity index 98% rename from stlearn/spatials/trajectory/pseudotime.py rename to stlearn/spatial/trajectory/pseudotime.py index f70e2a8f..4615617e 100644 --- a/stlearn/spatials/trajectory/pseudotime.py +++ b/stlearn/spatial/trajectory/pseudotime.py @@ -6,8 +6,8 @@ from sklearn.neighbors import NearestCentroid from stlearn.pp import neighbors -from stlearn.spatials.clustering import localization -from stlearn.spatials.morphology import adjust +from stlearn.spatial.clustering import localization +from stlearn.spatial.morphology import adjust from stlearn.types import _METHOD diff --git a/stlearn/spatials/trajectory/pseudotimespace.py b/stlearn/spatial/trajectory/pseudotimespace.py similarity index 100% rename from stlearn/spatials/trajectory/pseudotimespace.py rename to stlearn/spatial/trajectory/pseudotimespace.py diff --git a/stlearn/spatials/trajectory/set_root.py b/stlearn/spatial/trajectory/set_root.py similarity index 97% rename from stlearn/spatials/trajectory/set_root.py rename to stlearn/spatial/trajectory/set_root.py index 7c9ce806..65287ec1 100644 --- a/stlearn/spatials/trajectory/set_root.py +++ b/stlearn/spatial/trajectory/set_root.py @@ -1,7 +1,7 @@ import numpy as np from anndata import AnnData -from stlearn.spatials.trajectory.utils import _correlation_test_helper +from stlearn.spatial.trajectory.utils import _correlation_test_helper def set_root(adata: AnnData, use_label: str, cluster: str, use_raw: bool = False): diff --git a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py b/stlearn/spatial/trajectory/shortest_path_spatial_PAGA.py similarity index 98% rename from stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py rename to stlearn/spatial/trajectory/shortest_path_spatial_PAGA.py index 8daef15b..958907bc 100644 --- a/stlearn/spatials/trajectory/shortest_path_spatial_PAGA.py +++ b/stlearn/spatial/trajectory/shortest_path_spatial_PAGA.py @@ -1,6 +1,6 @@ import networkx as nx -from stlearn.plotting.utils import get_node +from stlearn.pl.utils import get_node from stlearn.utils import _read_graph diff --git a/stlearn/spatials/trajectory/utils.py b/stlearn/spatial/trajectory/utils.py similarity index 100% rename from stlearn/spatials/trajectory/utils.py rename to stlearn/spatial/trajectory/utils.py diff --git a/stlearn/spatials/trajectory/weight_optimization.py b/stlearn/spatial/trajectory/weight_optimization.py similarity index 100% rename from stlearn/spatials/trajectory/weight_optimization.py rename to stlearn/spatial/trajectory/weight_optimization.py diff --git a/stlearn/spatials/__init__.py b/stlearn/spatials/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stlearn/tl.py b/stlearn/tl.py deleted file mode 100644 index b7bb3cb7..00000000 --- a/stlearn/tl.py +++ /dev/null @@ -1,10 +0,0 @@ -from .tools import cache, clustering -from .tools.label import label -from .tools.microenv import cci - -__all__ = [ - "cache", - "clustering", - "cci", - "label", -] diff --git a/stlearn/tl/__init__.py b/stlearn/tl/__init__.py new file mode 100644 index 00000000..164fe27b --- /dev/null +++ b/stlearn/tl/__init__.py @@ -0,0 +1,13 @@ +# stlearn/tl/__init__.py + +from . import cache +from . import clustering +from . import cci +from . import label + +__all__ = [ + "cache", + "clustering", + "cci", + "label", +] \ No newline at end of file diff --git a/stlearn/tools/cache/__init__.py b/stlearn/tl/cache/__init__.py similarity index 100% rename from stlearn/tools/cache/__init__.py rename to stlearn/tl/cache/__init__.py diff --git a/stlearn/tools/cache/anndata.py b/stlearn/tl/cache/anndata.py similarity index 100% rename from stlearn/tools/cache/anndata.py rename to stlearn/tl/cache/anndata.py diff --git a/stlearn/tl/cci/__init__.py b/stlearn/tl/cci/__init__.py new file mode 100644 index 00000000..5716bf1d --- /dev/null +++ b/stlearn/tl/cci/__init__.py @@ -0,0 +1,4 @@ +from .analysis import adj_pvals, grid, load_lrs, run, run_cci, run_lr_go +from .het import get_edges + +__all__ = ["load_lrs", "grid", "run", "adj_pvals", "run_lr_go", "run_cci", "get_edges"] diff --git a/stlearn/tools/microenv/cci/analysis.py b/stlearn/tl/cci/analysis.py similarity index 100% rename from stlearn/tools/microenv/cci/analysis.py rename to stlearn/tl/cci/analysis.py diff --git a/stlearn/tools/microenv/cci/base.py b/stlearn/tl/cci/base.py similarity index 100% rename from stlearn/tools/microenv/cci/base.py rename to stlearn/tl/cci/base.py diff --git a/stlearn/tools/microenv/cci/base_grouping.py b/stlearn/tl/cci/base_grouping.py similarity index 100% rename from stlearn/tools/microenv/cci/base_grouping.py rename to stlearn/tl/cci/base_grouping.py diff --git a/stlearn/plotting/__init__.py b/stlearn/tl/cci/databases/__init__.py similarity index 100% rename from stlearn/plotting/__init__.py rename to stlearn/tl/cci/databases/__init__.py diff --git a/stlearn/tools/microenv/cci/databases/connectomeDB2020_lit.txt b/stlearn/tl/cci/databases/connectomeDB2020_lit.txt similarity index 100% rename from stlearn/tools/microenv/cci/databases/connectomeDB2020_lit.txt rename to stlearn/tl/cci/databases/connectomeDB2020_lit.txt diff --git a/stlearn/tools/microenv/cci/databases/connectomeDB2020_put.txt b/stlearn/tl/cci/databases/connectomeDB2020_put.txt similarity index 100% rename from stlearn/tools/microenv/cci/databases/connectomeDB2020_put.txt rename to stlearn/tl/cci/databases/connectomeDB2020_put.txt diff --git a/stlearn/tools/microenv/cci/go.R b/stlearn/tl/cci/go.R similarity index 100% rename from stlearn/tools/microenv/cci/go.R rename to stlearn/tl/cci/go.R diff --git a/stlearn/tools/microenv/cci/go.py b/stlearn/tl/cci/go.py similarity index 95% rename from stlearn/tools/microenv/cci/go.py rename to stlearn/tl/cci/go.py index ee7b98fe..6abfd71a 100644 --- a/stlearn/tools/microenv/cci/go.py +++ b/stlearn/tl/cci/go.py @@ -2,7 +2,7 @@ import os -import stlearn.tools.microenv.cci.r_helpers as rhs +import stlearn.tl.cci.r_helpers as rhs def run_GO(genes, bg_genes, species, r_path, p_cutoff=0.01, q_cutoff=0.5, onts="BP"): diff --git a/stlearn/tools/microenv/cci/het.py b/stlearn/tl/cci/het.py similarity index 99% rename from stlearn/tools/microenv/cci/het.py rename to stlearn/tl/cci/het.py index c558a441..30043659 100644 --- a/stlearn/tools/microenv/cci/het.py +++ b/stlearn/tl/cci/het.py @@ -7,7 +7,7 @@ from numba import jit, njit, prange from numba.typed import List -from stlearn.tools.microenv.cci.het_helpers import ( +from stlearn.tl.cci.het_helpers import ( add_unique_edges, edge_core, get_between_spot_edge_array, diff --git a/stlearn/tools/microenv/cci/het_helpers.py b/stlearn/tl/cci/het_helpers.py similarity index 100% rename from stlearn/tools/microenv/cci/het_helpers.py rename to stlearn/tl/cci/het_helpers.py diff --git a/stlearn/tools/microenv/cci/merge.py b/stlearn/tl/cci/merge.py similarity index 100% rename from stlearn/tools/microenv/cci/merge.py rename to stlearn/tl/cci/merge.py diff --git a/stlearn/tools/microenv/cci/perm_utils.py b/stlearn/tl/cci/perm_utils.py similarity index 100% rename from stlearn/tools/microenv/cci/perm_utils.py rename to stlearn/tl/cci/perm_utils.py diff --git a/stlearn/tools/microenv/cci/permutation.py b/stlearn/tl/cci/permutation.py similarity index 100% rename from stlearn/tools/microenv/cci/permutation.py rename to stlearn/tl/cci/permutation.py diff --git a/stlearn/tools/microenv/cci/r_helpers.py b/stlearn/tl/cci/r_helpers.py similarity index 100% rename from stlearn/tools/microenv/cci/r_helpers.py rename to stlearn/tl/cci/r_helpers.py diff --git a/stlearn/tools/clustering/__init__.py b/stlearn/tl/clustering/__init__.py similarity index 100% rename from stlearn/tools/clustering/__init__.py rename to stlearn/tl/clustering/__init__.py diff --git a/stlearn/tools/clustering/annotate.py b/stlearn/tl/clustering/annotate.py similarity index 88% rename from stlearn/tools/clustering/annotate.py rename to stlearn/tl/clustering/annotate.py index d1dfabf2..40cea6e8 100644 --- a/stlearn/tools/clustering/annotate.py +++ b/stlearn/tl/clustering/annotate.py @@ -2,7 +2,7 @@ from bokeh.io import output_notebook from bokeh.plotting import show -from stlearn.plotting.classes_bokeh import Annotate +from stlearn.pl.classes_bokeh import Annotate def annotate_interactive( diff --git a/stlearn/tools/clustering/kmeans.py b/stlearn/tl/clustering/kmeans.py similarity index 79% rename from stlearn/tools/clustering/kmeans.py rename to stlearn/tl/clustering/kmeans.py index e055b15a..e689c70f 100644 --- a/stlearn/tools/clustering/kmeans.py +++ b/stlearn/tl/clustering/kmeans.py @@ -13,7 +13,7 @@ def kmeans( n_init: int = 10, max_iter: int = 300, tol: float = 0.0001, - random_state: int | np.random.RandomState = None, + random_state: int | np.random.RandomState | None = None, copy_x: bool = True, algorithm: str = "lloyd", key_added: str = "kmeans", @@ -24,24 +24,27 @@ def kmeans( Parameters ---------- - adata + adata: AnnData Annotated data matrix. - n_clusters + n_clusters: int, default = 20 The number of clusters to form as well as the number of centroids to generate. - use_data + use_data: str, default = "X_pca" Use dimensionality reduction result. - init - Method for initialization, defaults to 'k-means++' - max_iter + init: str, default = "k-means++" + Method for initialization, defaults to 'k-means++'. + n_init: int, default = 10 + Number of time the k-means algorithm will be run with different + centroid seeds. + max_iter: int, default = 300 Maximum number of iterations of the k-means algorithm for a single run. - tol + tol: float, default = 0.0001 Relative tolerance with regard to inertia to declare convergence. - random_state + random_state: int | np.random.RandomState | None, default = None Determines random number generation for centroid initialization. Use an int to make the randomness deterministic. - copy_x + copy_x: bool, default = True When pre-computing distances it is more numerically accurate to center the data first. If copy_x is True (default), then the original data is not modified, ensuring X is C-contiguous. If False, the original data @@ -49,13 +52,13 @@ def kmeans( numerical differences may be introduced by subtracting and then adding the data mean, in this case it will also not ensure that data is C-contiguous which may cause a significant slowdown. - algorithm + algorithm: str, default = "lloyd" K-means algorithm to use. The classical EM-style algorithm is "lloyd". The "elkan" variation can be more efficient on some datasets with well-defined clusters, by using the triangle inequality. - key_added + key_added: str, default = "kmeans" Key add to adata.obs - copy + copy: bool, default = False Return a copy instead of writing to adata. Returns ------- diff --git a/stlearn/tools/clustering/louvain.py b/stlearn/tl/clustering/louvain.py similarity index 100% rename from stlearn/tools/clustering/louvain.py rename to stlearn/tl/clustering/louvain.py diff --git a/stlearn/tl/label/__init__.py b/stlearn/tl/label/__init__.py new file mode 100644 index 00000000..8355dd25 --- /dev/null +++ b/stlearn/tl/label/__init__.py @@ -0,0 +1,7 @@ +from .label import run_singleR, run_rctd, run_label_transfer + +__all__ = [ + "run_singleR", + "run_rctd", + "run_label_transfer", +] diff --git a/stlearn/tools/label/label.py b/stlearn/tl/label/label.py similarity index 99% rename from stlearn/tools/label/label.py rename to stlearn/tl/label/label.py index 92a8f1a5..96b5e84b 100644 --- a/stlearn/tools/label/label.py +++ b/stlearn/tl/label/label.py @@ -7,7 +7,7 @@ import numpy as np import scanpy as sc -import stlearn.tools.microenv.cci.r_helpers as rhs +import stlearn.tl.cci.r_helpers as rhs def run_label_transfer( diff --git a/stlearn/tools/label/label_transfer.R b/stlearn/tl/label/label_transfer.R similarity index 100% rename from stlearn/tools/label/label_transfer.R rename to stlearn/tl/label/label_transfer.R diff --git a/stlearn/tools/label/rctd.R b/stlearn/tl/label/rctd.R similarity index 100% rename from stlearn/tools/label/rctd.R rename to stlearn/tl/label/rctd.R diff --git a/stlearn/tools/label/singleR.R b/stlearn/tl/label/singleR.R similarity index 100% rename from stlearn/tools/label/singleR.R rename to stlearn/tl/label/singleR.R diff --git a/stlearn/tools/__init__.py b/stlearn/tools/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stlearn/tools/label/__init__.py b/stlearn/tools/label/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stlearn/tools/microenv/__init__.py b/stlearn/tools/microenv/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stlearn/tools/microenv/cci/__init__.py b/stlearn/tools/microenv/cci/__init__.py deleted file mode 100644 index efea98c1..00000000 --- a/stlearn/tools/microenv/cci/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# from .base import lr -# from .base_grouping import get_hotspots -# from . import het -# from .het import edge_core, get_between_spot_edge_array -# from .merge import merge -# from .permutation import get_rand_pairs -from .analysis import adj_pvals, grid, load_lrs, run, run_cci, run_lr_go - -__all__ = [ - "load_lrs", - "grid", - "run", - "adj_pvals", - "run_lr_go", - "run_cci", -] diff --git a/stlearn/tools/microenv/cci/databases/__init__.py b/stlearn/tools/microenv/cci/databases/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 8c568a56..02f75209 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -33,9 +33,6 @@ def Read10X( In addition to reading regular 10x output, this looks for the `spatial` folder and loads images, coordinates and scale factors. Based on the - `Space Ranger output docs.bk`_. - - _Space Ranger output docs.bk: https://support.10xgenomics.com/spatial-gene-expression/software/pipelines/latest/output/overview Parameters diff --git a/tests/test_CCI.py b/tests/test_CCI.py index 720f0181..7dc639f5 100644 --- a/tests/test_CCI.py +++ b/tests/test_CCI.py @@ -8,8 +8,8 @@ from numba.typed import List import stlearn as st -import stlearn.tools.microenv.cci.het as het -import stlearn.tools.microenv.cci.het_helpers as het_hs +import stlearn.tl.cci.het as het +import stlearn.tl.cci.het_helpers as het_hs from tests.utils import read_test_data global adata diff --git a/tests/test_cluster_plot.py b/tests/test_cluster_plot.py index 3d9280f8..6711ea05 100644 --- a/tests/test_cluster_plot.py +++ b/tests/test_cluster_plot.py @@ -9,7 +9,7 @@ import numpy as np import pandas as pd -from stlearn.plotting.classes import ClusterPlot +from stlearn.pl.classes import ClusterPlot from .utils import read_test_data From 1f2b43370c342c30d93337869905fda01a50e4e7 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 13:30:40 +1000 Subject: [PATCH 141/241] Update. --- HISTORY.rst | 2 +- docs/release_notes/1.1.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2714496b..815cb9dd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,7 +16,7 @@ API and Bug Fixes: * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. * Removed datasets.example_bcba - Replaced with wrapper for scanpy.visium_sge. -* Moved spatials directory to spatial. +* Moved spatials directory to spatial, cleaned up pl and tl packages. 0.4.11 (2022-11-25) ------------------ diff --git a/docs/release_notes/1.1.0.rst b/docs/release_notes/1.1.0.rst index e255fdd0..bd32dc46 100644 --- a/docs/release_notes/1.1.0.rst +++ b/docs/release_notes/1.1.0.rst @@ -16,4 +16,4 @@ * pl.cluster_plot - Does not keep colours from previous runs when clustering. * pl.trajectory.pseudotime_plot - Fix typing of cluster values in .uns["split_node"]. * Removed datasets.example_bcba - Replaced with wrapper for scanpy.visium_sge. -* Moved spatials directory to spatial. \ No newline at end of file +* Moved spatials directory to spatial, cleaned up pl and tl packages. \ No newline at end of file From a0f8239b7eba251807022426142dd984f83b1acf Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 13:38:57 +1000 Subject: [PATCH 142/241] Install dev and test dependencies. --- .github/workflows/python-package.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5f5cc50d..a0935c6d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -26,8 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -e .[dev,test] - name: Check style run: | black stlearn tests From d38e1dc25242583f87d2e014df577056b28a8ec8 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 13:46:56 +1000 Subject: [PATCH 143/241] Fix formatting --- stlearn/spatial/__init__.py | 8 ++------ stlearn/tl/__init__.py | 7 ++----- stlearn/tl/label/__init__.py | 2 +- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/stlearn/spatial/__init__.py b/stlearn/spatial/__init__.py index 017501be..d7034329 100644 --- a/stlearn/spatial/__init__.py +++ b/stlearn/spatial/__init__.py @@ -1,10 +1,6 @@ # stlearn/spatial/__init__.py -from . import SME -from . import clustering -from . import morphology -from . import smooth -from . import trajectory +from . import SME, clustering, morphology, smooth, trajectory __all__ = [ "clustering", @@ -12,4 +8,4 @@ "trajectory", "morphology", "SME", -] \ No newline at end of file +] diff --git a/stlearn/tl/__init__.py b/stlearn/tl/__init__.py index 164fe27b..eb55fc20 100644 --- a/stlearn/tl/__init__.py +++ b/stlearn/tl/__init__.py @@ -1,13 +1,10 @@ # stlearn/tl/__init__.py -from . import cache -from . import clustering -from . import cci -from . import label +from . import cache, cci, clustering, label __all__ = [ "cache", "clustering", "cci", "label", -] \ No newline at end of file +] diff --git a/stlearn/tl/label/__init__.py b/stlearn/tl/label/__init__.py index 8355dd25..f07ffcf5 100644 --- a/stlearn/tl/label/__init__.py +++ b/stlearn/tl/label/__init__.py @@ -1,4 +1,4 @@ -from .label import run_singleR, run_rctd, run_label_transfer +from .label import run_label_transfer, run_rctd, run_singleR __all__ = [ "run_singleR", From 2fc734394a247ba4a066a858d94d34fa97e99e63 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 13:52:19 +1000 Subject: [PATCH 144/241] Remvoe todo. --- TODO.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 8da59d28..00000000 --- a/TODO.md +++ /dev/null @@ -1,7 +0,0 @@ -# TODO - -[] > Python 3.8 -[] Fix quality issues -[] Replace tensorflow with Pytorch -[] Upgrade dependencies - [] Numba \ No newline at end of file From 40d0397172912e5617dd3ae90c3a00efa9f5290b Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 15:36:16 +1000 Subject: [PATCH 145/241] Bump due to network error. --- pyproject.toml | 2 +- stlearn/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d6e72578..ddb5e570 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.1.0" +version = "1.1.1" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 213fe82f..092a2918 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -2,7 +2,7 @@ __author__ = """Genomics and Machine Learning Lab""" __email__ = "andrew.newman@uq.edu.au" -__version__ = "1.1.0" +__version__ = "1.1.1" from . import add, datasets, em, pl, pp, spatial, tl, types from ._settings import settings From 2f78ce67ef45adb0590681e43a3f091f68876fe1 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 7 Jul 2025 16:02:08 +1000 Subject: [PATCH 146/241] Make it 1.1.1. --- HISTORY.rst | 2 +- docs/release_notes/{1.1.0.rst => 1.1.1.rst} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/release_notes/{1.1.0.rst => 1.1.1.rst} (97%) diff --git a/HISTORY.rst b/HISTORY.rst index 815cb9dd..191c3c6b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -1.1.0 (2025-07-02) +1.1.1 (2025-07-07) ------------------ * Support Python 3.10.x * Added quality checks black, ruff and mypy and fixed appropriate source code. diff --git a/docs/release_notes/1.1.0.rst b/docs/release_notes/1.1.1.rst similarity index 97% rename from docs/release_notes/1.1.0.rst rename to docs/release_notes/1.1.1.rst index bd32dc46..3df3044a 100644 --- a/docs/release_notes/1.1.0.rst +++ b/docs/release_notes/1.1.1.rst @@ -1,4 +1,4 @@ -1.1.0 `2025-07-02` +1.1.1 `2025-07-07` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. rubric:: Features From e6c52cfd5fd42c0736c1744e5d350daed79a4806 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 06:33:18 +0000 Subject: [PATCH 147/241] Bump pillow from 11.2.1 to 11.3.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.2.1 to 11.3.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.2.1...11.3.0) --- updated-dependencies: - dependency-name: pillow dependency-version: 11.3.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c11dc46..eb5fccfe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ leidenalg==0.10.2 louvain==0.8.2 numba==0.58.1 numpy==1.26.4 -pillow==11.2.1 +pillow==11.3.0 scanpy==1.10.4 scikit-image==0.22.0 tensorflow==2.14.1 From e78c1e0e86323ca0c3017a7b1416f33ca1e37aa7 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 8 Jul 2025 09:08:33 +1000 Subject: [PATCH 148/241] Make it 1.1.1. --- docs/index.rst | 2 +- docs/release_notes/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 142e73d8..6a7c8ee6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,7 @@ undissociated tissue sample. Latest Additions ---------------- -.. include:: release_notes/1.1.0.rst +.. include:: release_notes/1.1.1.rst .. include:: release_notes/0.4.6.rst diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 6194c62c..25e378da 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,7 +1,7 @@ Release Notes =================================================== -.. include:: 1.1.0.rst +.. include:: 1.1.1.rst .. include:: 0.4.6.rst From d451d2a9107d0ff9a5a82c8f03417745d5039bb5 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 8 Jul 2025 09:23:29 +1000 Subject: [PATCH 149/241] Add images. --- docs/conf.py | 4 ++++ docs/images/any.png | Bin 0 -> 119415 bytes docs/images/scanpy.png | Bin 0 -> 172898 bytes 3 files changed, 4 insertions(+) create mode 100644 docs/images/any.png create mode 100644 docs/images/scanpy.png diff --git a/docs/conf.py b/docs/conf.py index ccda1308..80b991b6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -70,6 +70,10 @@ def setup(app): nbsphinx_execute = 'never' # Don't re-execute notebooks nbsphinx_allow_errors = True # Allow notebooks with errors +nbsphinx_thumbnails = { + "tutorials/working_with_scanpy": "images/scanpy.png", +} + # Autosummary autosummary_generate = True autosummary_imported_members = True diff --git a/docs/images/any.png b/docs/images/any.png new file mode 100644 index 0000000000000000000000000000000000000000..4b010dc18252a4d686293e8fb51668746b310c9f GIT binary patch literal 119415 zcmXtfRZtvl*DcQA4#C|Wg8Sg^?hZq6cN;7?1b26b;O-%~I|K_7-1X=EPMwRcuI`(z zXZ6;#*N#$Al14!!K!kvRK#`S^P=kPg(t&`0+yTISzVXJx(*3+ax~fS7A!?_Hjz1q@ zti=??ARrpzkzP$iM*4owBBQ5maVMWq;nA2I;kg>>rPsH1W+u zUN(%W)@WjU>H30OHOXr`FjR$3)6ZdAh$5qOE>@BdGJIH>#}~Np%bT}L_0hL=VLIIg zH=~`BuGq??rSsH!inIlUua!mI52>Et3YGCxbm>)C&tJ^SQ1ZNL88lS=Lz=r2!g%@l zSr&`N$e#@%6#~`Ra~QR=4vX&S#5xT!0)BY^M_0$rt>?0Gj|*Mt(GLq9rlseM z8}M*QCP1ZXo>miaS-DnFuKi1;OWOdYTE@4xGY%8i!c65tm0x}H4V3|N5|rgKFL;OX zgIFXIe0d4%jlbEc%NHxJZNLfgaf6HPN{^{!?@r}EDis#0 zQ`$7NPR1a&GwjzlffH*Z{Dka%NizNoa%%YrST3os-$YR2osy|*r`FoI6;VQ$NbHr^ zoNFQz;jiIuiHb^n#D?ZJMhaiSzi%PEEOu2>8YBuA=w$9|fPSv4e2+c~igi)ivpo0)iSMo!2tThS!8E(3B4Ag2|fN*sLz4e-8RF)Nctnd#Oy*kaU0IY<^ybXaw} zMr4^zoW^>-{U^F6)=Xk$`TDUt#CrAl7epIr#dI&e|5*<%8EIBfbSd3A!MDtW1=EH~ z?%%|JRZ;rnf=h)M$8#nf&__y5Z32mM(HS=QenOe}s%GifhjBY2xa5FjVLAlBYCb&y zb3+s4oSw+JqG?Q+b2X>Y)MIzQN|3wyjf$;NKab3IbrK|7uaItox$vv)Z`(SqF+E68 z&S{(>$2nUIiZ6311e$KXth!{M3N5}>-dwE(tHgi^wF;|654Hu@7Ar4oA=F})aldBs zt_@xEramQ8q6&Hu#5ndG-G4M_)?mwbvTgYZ9?bv1LvX6{rtXA@;8`Fr&ch2HqJl4D z{jLW38Bpt=58b+Hm5{8J!Jn2NGGj#x&G9?W9!Sn@WrKyETChlf1*u6g)V**I07!yLNN;~)c(^8i43p>EJ;zx-^C{&(bm+`p-Isbi->Dm?f@9$Z;3D} zyh$rGEV)}V?oSmd{x226!ia$%Q_mTPV*hgI4^u6RYHDc4I88l$5@nV8w_9rhMqm=- zb3z7x31+AMd@{;GLH=f!^3DoaHPfR6H4(5YYFWOnERAdwfDTaQX zU*gBLG=?|`?5iFW8~+|Py$aV9m|?K63NvUQEUz^8l9c%lC^5sw#jKJ+Zvkq(A3kX< zG{I$UhPtpvXU?N&=a&WaIM0lhsi9fuGBnTdePoySSFTN9{!+^p_^KoFu?DvnVGDMv zSa%U~DVh$ASycHz{ZX!{Hx9Gf!a%fz%9QJ)GI1xR#kEg6TK*=fYXbwv%F&3C&Nd72 zzn}*_HborFGZU}o;cPj!h;PysT8%E~xE%fohN!wVy)Z&r-TY-YMb!0EumC9>Ng!hC zX&q$kR+t_Q?2j+AT}H-A#76OKNIVHx1({f49zFKtz3y$HW7bC|Xt&=@Jo43adybP! z*uMcm*Z45}Jo!H|IE8h{XPAy}-O2g7(WuG2GZ*yy<5J;`BJA;qx*lg2b``ts;mE7kL7l_@^So z4Fnayxm*wh_g{$n_?N)@_62kR#ndR5qPxoLox9`VO2e9J$=|VL6py*bU$aALk*L`| zY$ok<#S8lCuTiCj=52T`booLt15Gj0bftbNy_}yNU!Y|%)lb0Gj}F>yL-N`Y>pKoV zu;(fA7*-k1H$6=?NF3HRS9lvSu}vvKU;oWi z#!PE$s%ojkx@e;aJ!MTUvv}c~-RPec^Gi?15P?QHPgxei-$J^-C~3_n)))0|E?g3p zhF0t>yyQi=VaWM%Q57y;GLwGfD)Jy5r?59sM59N4Eh0ps?L~96kccZ7F?Ss2?1R}3 zyJb4th4gnE7;^a^3N;DwIpKrnVUvX6Y~TM&``&Oj^fAI$q<_D(L-n6C(;fcdh^PZY z6hzIiR^22=PLqfpjV(HPs^nXr<}LS2XTGaT$c~Myp2u1dc4E20_rO~J`mW+cEJyyN zNCogBy2hft3|m(baFM8`uJX==-+n+fc}rkh;6+CqcWiH!P8{~^W2qf)U!R=C`NvUK*9bL zlR3;w@{2&GMJ)LIo-!{xM0_lG^Pn!U-eruI#&nS=eT#-0VZp+ty+86n{iQdq)}067 zeV%&1`^A3W=Lms($-@)9VpW{Z$~>~RLyoIya^U=uA}cq%&eQHK1_A2`E!iaF(Gi+o z*t+b#f4M;~RF#_wd@4+G6-Aqvz7=1PBT`J0a4(>#?!^929^s~pG+h3ie4}%att#gU zaMNulqZD?upWj|*mY$aj|CGCUAl9BFe!jH6yDb8J3syP3faM4K&Fy=h75ga%$Ty~9 znjYX31WruWSL4H#9;$TSt@r)@kx+wPj~;q(!l>%Ii6g4l^WxHkh!DmYwD77*O}!gz z7=AI;*swpH(!byDSZMu5*lf>mz>VYv`R4WomBQnhT>_33H-vUbVlq26)h@B4En#A6 zaJi{Hz#)w6Eis1=pj=<4q>yPN(7u#Js=>k)7X0r5NY6UGJ`o#5BdSi7{GByUI7a1y zo4kj4MV8@?Y_)i`iy;~#tvMC}YoZ^K=|&;l%bl#%3(?_lxhPMyhyeA8*mE`P*`qQj zR$0)}hR}k;ft5+mh+CK@y9~PCTI#KXTIBL=R6|FmlW1dcy9xV99*S0)_;sz zmNVPGMmK+e^A+fP25*gEX;xE_yeyjjqOp1`dl! z?DF0$Cl-JllH~0KCv+>g&IroaNRjiQI7TcStlk%i8cg-O0oBj~Tbsv0R3oIJiw%B9 zX9wbd%E6i}=r3p!yHN{+)|a^iFfVEWQ`xbMIj3x#fiMn^8tRZ-DEMdxA%c|9con4D zF3%7|EcmpnL{&8*F~n8qqH`oPT}`JULyjA}j4 zUey+srS3U8V<|I5B0*26Zu-(#U1%B!(a@>#dpN<=CUlGehf4EMNKiep-m5Mlx;-7e zIJ9q^ebC^@524I9iHTgXzeiEV5F2=2YFPhJgX^uS3Y0XH`eUBLL8OKBEr5q<75m%E z{Xp(?;UOM{wAs)g^TEhod8`sZJmKLe$^P0u^JeZwdXawPpvc70fNV}sevXj11G@w$ z2lG!1=ev)wJ>9<-F%8OD2OTblBd-XMw`b+vXS?Lu!;rcTf!||u$OG`{JKwLh>ckH(W+AcTnYOL5~*PT zwIT_uyjK#3r{DU$)6b5!W^QW(0H5&sm5iW+(Jg5C4~W0jM#&zHSM^Iyf;q!wn3k{G zNrc;#YpN-}F2AnMm}knW7+^d+RNoT_y~k-v4@bj*KB&~>lJ8UQmE)jVWhMfKU@D%K zPL!*tvXb=7ulk(>(3eKcK=k;2Jfo7*^`(qRJA(B2f5$4Zr*dSpcTVuWdh!bU{Ab&u z+gkWZMng&>l~w|q%&TfT&3Wps5nSRBds@y+&8Url*@gqL8nS7Lbwfx61DXGjKq7hC zFQzdsJv<(+E*6V~D2taKy(}4{p;>g3hQCd4FUKW^LB=4>fHvWVjGKiX|LiG?J26Z}(Ji2wlPBKA4>yX|jn|S- zy5wgxzF0KQ6i~M_5Xc8?8^!69#uz%{oJ$3kpUFdI+#S9sjIh!76z7+Ux{(cn=Yox9 z#=EIiFE#UNf0*ExuG6kl&BVsStqv!zfqUXKV=puW80luB*H{(>{R`HRMmk+qiqTfA zYaAHyv07YIa}y^Ljd)A5&~#XTn%aqy(&q{Le&eJ*9oD)Z#!RndKyfT5U41ILbNy^I z1V0OmKOxf8o8hSze$h)W{hNC*lHM@+b_!1%jw zt+^`8HmUp?QNuG9nrUWf=* zwPm<313mO()9%{3eo!SY1WUWZiw3%8>^pYbP{N*sPHqbO-xG530W4|CV++ZBkKhEx z_*Z7C@3s!-ZG@?vz3@ydbGzXlVmJZQz^$}vdYBHLLHy&9fy7P|<%tV08-ik}c0h6( z|D-N4uThh!y$gwmDeJEzr5{t(MeAV%6TDtPhvQ)y^&~8*@3j4e;vHJ`DNw4gKA2m7 z^=!YB#70<{zZTMT?&``I+M|-r-9(tA{q%@F_(IUgO~(4R z;{t0b+n^-_o$S7WBPiHsOs zc1o+5&dc+S6n9}ei_KXqxChHJ6JECdZ<8BMg$=|>+v!yw2|>a#dbJX*hOH%9|Ly~2 zvM^NOk;h6G@wGPucdH*AOraJFA(W-=0b$&~u8YUt0c}NZm+sYKcRLv3=8|RC6}CH< z2s=N}D>J-zj^A9))c>i*(g5kzCxcna3O0Ws*{=PBZL?rZpk^-@47f!uy3 zIV;*L2gu$ajIU`;Abr+-j8vzMAk^M72GgMj;DFT_{kAX~~V{>`{3EFf4&(=a=S(x{g!Xg3fvDPuOSxlKt{t3IWK0)& zbrFDF3g-=Sy`)|47k)9=1*PVnXZ=4Fk3>}9R}C35UoUAM~*mB~?+ z@r9@@Z-6g9Jgqe=-F!r@o&NuT161T}@95)9`5_JIkiV#AiBoGp>`;mYTLv{w%}z4QxzWgCrF1QM7j72v zRwpQrs#{ji@0Cwg-6jGJ)1!G$thSy1fQ=sc>4x8`-%k87^1KYrx5@Wx@qWdQwCHmq zlXe-HK^f6J3dbL<3;cXY04FRd5wx!9Q40Rdm2dd#1=b^n6=!rqqzf+2ZJpJJ>l0_0-35p91IK~z_?tmxz1f$AO zu1zjN?yx6=F1a83m7*oegjlZsD%ngnQLQkGpH+ww-`jF4HTGNLMmtI%_< zc-QoqQoei=BFV>a#gbWbGPK!$=6%9AK8S2Vdj&i!$S8rLwa!x;<~Zp&E`Pm&10I3wfib zu3vCO&f>#_U7P_+bIVsQ!y2*XftCAsUTNJit=g{Tb9^t*7f>C*aIL5V?~VH0TF&A&VL7{^@BrUO(uG)7@N3EvF7*aH&eX-rF}Y}}5>NRhmG zO&&T-F~m^XoGzz7VB+5|wg+AA#TqP@!p(Bv*yp}GQN<*ZzL>_mKE7b|*hKgli5dZv zs7GNIzLt0;M8#4Ni$tAsEf4-X<^GDIP%Fp}?e~s=VhI%J}BYY>w9-bf=@YYpL@Xbx1T&oeko?7-m0GE&~4) zC*)TIeQO9A#8q?`s|J46byt|ShnMnUtoql#oxxmh^#@ftM84Q8#$YClO%?ZQNXCqI75D+m>35-QvDUDE5*RC}~$ z{`?|d*S=ZG>*cI;tca9)yW`uESvp zqRDyV-&`Z7AG<1KBYm3cRVC2p5S?KaHbcS|O@C`) z6*X$hwQeBp1_s}tFCCeXH)aHhH$hq8%Y&+bPxEd~YxcV?B_=bdbpx2t;kXgI*n7M8 z*Y`9_g*i2WI4|L1RU&A}!aWZs_?@3TO0t~?L!zrL3>#$Jg)$Z`y2~O4X`3nuSaQVK z@M;8&`p9b{spO;wTegudC7?a6fS(!J z*?(ft`&nKxvEv6~W(;Fpx;vD=_%ov+r--QyH!QY3G0!dC-#^9Yz}~7CYH@Pke+ltl z-$h0ng8-Qvkt*uX(Uj#q2ALiY#pu-qqzsXI@H*E_`SIc62eFl$07o zij3DbqwC}oc`5Ok3VDyWtv@`0iO;rg=uuThHb95)XZEg|w?)7H5VPJ-VXeorg zp}uDIKoWBwgY*rbzhk)hJurn;{1-0|(LCzg`K>GI>`i zHQ!-=wpb-jTkp2$g!Gjc90}>#IX0Bc;*K1D3V*S{Ha`%-R2;@;kjcPMyU8c}R!2}Q z5edV;H1=4Uj+PTkK-f6ZAY=+)&c_W2@2YM99R~js&VNcIe{1lm8mEW#qAgPcDcd zkF%`HxMG0?$zT!N{Abm-6v4@b;L8_?fRhLaP{Ch#8sl!Gzo`GdW8C!*&4yj)Oe(Ubc2vI1r7$wB#0D#nnCx7Nxh z4gYlT?N47Gs;JG^zk^O~uYh}d@r+LXEIRc(cgF}!?(p|78&~3)J>T!_@Z9#mi*eaZ zF#el_r`De{5ZnK%kSeI`eY;f{qFl1CUgn`8^29@ye~gHHk>6`koW`mAN(12{1pKSM zv?A8hO`DulT+Sxcas`t+d}G&KL_1`M=gzGk9T09pF}7V^^1% z{@0RsUE+%+o-%@MmZT1i;bukgpaIRe=ocqI0X=QQL)P|yf8Hy0?q?m|KnD|6{5>3= zT9ca)=)!ibqFezEdd>B;{6b?yz!Vmamc)SBJab9rv-Rk6+w~47;o%S(IOxOIiJi|@ z)sI&dR)`+d=p1h?7>8^t+MkxaM!VepWyu7u^0Pv`e2a+P3_zs??Xoe=qBF6eRZ^2H z?qStWylEM-XiT@IhTxb0!d!4#vK%35S0Ir*p;`QujVcx+UjLz9vci{ zkHpih%5UOe9J`3J}_@c8gtetW=dw;^=lI{rF`BaG1V6hT7WOK%O=_NtCZWC z5f5ATt@%GnkAD)siDB;d4~$>*V-bYcnM+U>@~q<_;_DlLRVL&y`P*)Ud}{enwpT@6 zn_^bgAaSa2&M!^gx<2{c@Xt|Q*PpRrr9l>~TGMx%4c0x~v1$C)Nc`8F9+pQ6%6i@! zAh59f5bx^JS~&XRb#Z&IFF3Svvw$`%l@5x=q_s#nQq!>{BoZCxB+ACf)zhsHGYRKx zJek$S<;c;S_yi(S#uM*(%N^<2!5gWgwg>yZPxLLBsB##&vOT2w&l6IAJD32rKma^+ z2B~qz1s?fV9v}`_QX#GPNa?ySTEdy#DeNekIT!uchoRHsUFK})>wn6>o!nO@Rbv$iY09X@;^{trdFN3fql zX2VbQH_3aPMnz2Sq+{g^TT5{ILKw?r($`(~S+@D3t}~%rw39b6MTKv#&dA>aTeB-| za`aGbU9%>`!e-$Yd&dY#94cg!{3oLVT`tn{ZcG{GhL$o<8BhEZGX z$sxxkdX*i#)Pq4+ayircLD+MLM>^WSN2?K|5-*WyZXLrwt|@*^cZ*QrGJZNo!0UF! za{F=VTYq0B68QtYQJ}6v)>~>*^r8Ca_sp7eyV`*0S2p#zWp-yW-=E>kv9Sfp-aWa z-5fz8x|K?26r;^|5bhu}k48jM<&3Ax22q4|a+%mwdA`nm<7xk}bCUCQuk9|li+h=y zaw}DuOipK_gXCxyt_d~0K2x!`^f~_m=kcD9V2fXNwudWbu0DQ;t1MD@sQ6Q#^YEi7 zG*dV}uB75^TlC_P=akn$*9h1i$g*VP(+u13^*!a&(vdzV8D5f!8A8bMW#U}gH7)^G zCMt)J%}?+uig|tN#xML1AH5%+RJ&k}l|TnaA}()5O_I4{Nr)1;mb@NapeG(&;3ct- zn~^^?lT!FMGMC5mlx5TB-O;ZzbY+(btl+6r53|Y7o&av^p3+4Mpu`pG0>a<|MSPiQ zq922~xczL+2!=t31zg(sNl^$*MeQ;qo19@O>c3cIGmKwp*l2-y&P{W=e6(Dh$9g-J zhSx@sxYIdL0*Dy&q2pfoKbm~s6a_2RGVp@^C61mmUwhKiPP2{QQnQorq)36}d|~hZ zE*$*x56GmG+;Zp3xChxvR2#8n*>_yprTc=P7RF1o4WrR1L<;A)+3tAPm%^3My5<`$5xJX zfrQxEbNP@4fB|2In$^Z^9>Ir<=8sG`O*bb+hmQKj3*_ci6#iT6eqjl1ylV6Ci^XNr zGMow9cHfJCGi}Uj-X(tq4Uzu^4RLn%0xMj1prgxmiE^@`OtKJ12+$8B@1rEI|H8nB z_`LUq#NDWD>6cZjdDD7qR!Oxy5-e|+j&shs^S{Nm_blT9!Uv)7*xu(XCosQ?QcOX! z69LXhY|@3RrZMjB=0~l*(|qrnkdfzu{mR`{Z)nLx6sRyLGa*ARm4?;3SdT@f?FQpg za*2%5euQp|RSAW{*yJ&67>r??Bby#gA-a7a#v(juNufTE2QhN{0~eqqsWj6oRGtCJ1gLk zqa}yC$9I*xyPffAKg`~C4*}tuhmXQg0WxXb$VI(Y_&U5`7jG7L`G!buzHRr(l(ws% ztUJ#y7m^oQ+sw%M)tQxMXG{ciPt24xY01vj&pXpar{S$+Jf7&Q>==LhHR4l6ToN z1-qn;YN&BZR#332`V6eqtis+oHHcQrdI_0nNRg-OFC-pBQ2cSy)N_6bY2CmaDSeCr=)OnP(zy7i9g;wF8Jf{hm%wtcUNc zbOF+NgTXb|J;pMcQsMmaryI?Xo$qIZe|<+^_F2i5+I4?$>Sn~yQ%#AZkGa!<&B{nq zaePF&FwQSQMWqFC)bQs+LnL5$i3F$9#|)_S+4<2h17acAvo)rab8MFsY+d^JN8NM# zan|qNP{&joE%$IgT(7v7_xMOlk10L#15cTyh-g6UnkKBI*tNIqdPM%{?^Cf(&z)dK zo`CKf*olMgysaBa7#~l{Mi39fhC=?n1p<}Q-ExWe$9(e0_MP_Kovt9msI&XGx*5Je z*ivg^_#T$u#NAjWK-@RHUS(XYuJIBd!CvCDa~biC6Vg{wyCLntjV`A%flF}5LB4Hg z>|sAAx&5%0Y5P9ap>hCePF3BKukF~I=m^%uI{rxnXP*f(jRF(tCu!2~XKZxj2aGD+ zQY95&BF~S{mOoUr52LSOy*DKOV@%on8#`P9_6aGDQ+vH4&7C{2S`Ik$SBdyB#>MXrlnmEPrAYkL|#3rx82fjGoM;%c?cB3yi0a9!?W zVx;1H)prkTr0x{c7l}j+Vm~g{yCRi|!+183S^;-Blr&IPOe{>OBBf}4&@B4MEb;my z>4I6mgLSK9IFV#`eaYjgEO5OY?6}C27er4v4N`Zt(Srw$Cee=i&?q+o>E%{si*gxZ zw#w9T4LIR@E26_hr>%(bT(Cy)rm(*9;9AbkwkIMqe#+L6|Km}&@{MJDo`V2?;9fC! ze@i4>wt8fVtFQgInU+KsalGW`=ocs)EI&+oymoxY?RoY5s3x7AuKWzO-9!S6IBoLO zzkl&{&PV+tecsl25DCE4R91zFd`)eCZEG~NdQy0mS8L>bMhCuiF7y@yY4>q3!3;46 zB!A46`|u25J#FgGVf^8|7+D`QCf@XjNq!SQ)$(UWys&bIbk)iTA-hP&j_&v9W1O$k zfD6%L`dbVV(tm^%A0+Rsk;<)U_f=cv)A&^)wcJAf7P;j9(WGAg;^Xc|Kq{}_D0E=b zd%S%{RQ1}uAaWv4n>XI_Dc;+l6p4-_?q!`0k14sSk7-vr~-x(wg)`6;fRVmr2s!F|4^WX^h~SbGol{7jMRSe7h(qFKHq< zy$0C^v)IwtQO zQxMsPY?leGy?qJY_Sf-)6-u9F(QYVyyDXE|(XHl^amn1fe$|lTxAoVAgPqz35J5u9 z!dLX))I5Q&Mv9)8XPbpqg9jlJVAZZ<&WM@rT^dFDW=yLo>hjY5?Ha!Oa(_g$f83EL z9HyF0YqF@Qw~oGPOi+4FLLUd@Uk6W?&`F}C1JE;~1kgJklbyUeRhUO~#Gb;@sqWYf z<{+*dK>;V3*(-S^5181$OasR~jGaUzK-Ek^c5j-@yyF3aJvpiv*qUtE$cbG5j zA2oR+EWV}511q0P5*fcV5lL+%VKyElx;lWFOud-J9@9_z24L}`%vI;VQlq_LnEAd# z5ik2;^dyo>P)`uIF{+|beueS>V-t>g?=mO_9nBlECd;F~mmS_}UBw=l!PcPg9~0tD z-suFTgkj|d!Btqpig(+6MUF=*&CM5%s%Yx^E)h#3*1293cb2Ry7}@JL>g(j6g^5se*5uXfu9Cr=F^(|q=D|J=j#Q*qPin;!8GGT zL&=$dmF0FLuFy{$K4V^|yKy(asFj?zEezZ|(jgLG3HaqXSk$kT`0Vz<{K zlFZ6;5|i^tVb4e(!sZ+Dq&h?-UZKrBFLgJA|o@tj2*+diiU?#%3ZK>>5g2Rgq zX7FNs93e^2CW(svi($+9)!Ujsuf}>b6UQ~xs!81{Ixyja%>sYqppBq56KkDW0Nn-o z?}gce(RT$?0--uzg_A?^8nU6*PpZ#e#$HocDD?JB)vdY%$h9!}EnlUn=9Lkf^U|ZH zDv!(b&+C7xGgc%A&*>?WV4klk(;?fB;OR}H=rU5=q&bAZBR%)pfc8^%n9xDepkdudzmE1b2h=nl>eRLF+Y9#t#5I=j(7;0gpujK?;p1>y zl$x_NuLXW?3pWCNSeWYJm46QY18z3!Jgn@7Kep|qV(p`Uds#R<=ruYY8udxL1TFQI zalH3CTSeDvQ-7tk?`*u`^#$nS#d$RZ@3^y0B4F|bz5Oh?{Gha0*`~vOo|D4frq%ag z^)dIidg!H;R)IlMdr8K%1yO@CQiblypnv5o$EPNz-d{&0@KXh-o0ubjT-STG0q%9{R}kU4w%64Ln&9D7A6-?t)QO8&=; zU6K@5w8ufN8Id4xh316TBmL_*<)^K&VRdf6kHd4Z*9K#3;6oOm`URg{Thp)OUe&Mh ze)&s3L4lU)qL%Z9YUuC-%hORRb)4~g`%bX3k-Cf-TJ8|>0joLrml2i;E%5yZC!%4e zX}keguR$mpU3eW;8{#;u2y~Bne^KrzXxqrtuKn!D=Vz*$u6TN`@mUy#rm)3id7*@= zR@GEmIts6K7}7B5Gk;3xu~6elGoJS9$1CT_Xj#LPs)EF7&mWVwN4Nd|#(yOc2)oH& zk6qTTiUhP5Mnq6z4^4UoKxYM9#H%th74mw@O}ThY{L%s#TD_NN(xF&`3e3J%IX61) zkkJ?G#!^KTV5S>MX}2n-QyQT%Md&q}`6$W!fz!>gKG2#92s{eZ3CqUby_Nb(EN}G= zey(k9BB{hlc$bk6tXImN5xo)qHlSST5*xQW@kyB$CS-D-%;FBn|4Tt1FoQY{B@M1I z7kae|fZ+nwPC;!2Yo1soUmZHF+|{omhdlX>ZEK$Yni%XoK)b!Teg9DI7!zMieck-i zAw#N9#9ozD^kFamS_w36#QUMltW{GxNpV$3&m5GG z+1Rf~>cIFa5MVmb?iW~$v^6RMbqLkMJE-SSC5FY$<74ruNC$Ceuj$Vl=}DjKq+V&r zAOv%pn&JO9_Hm`?NsUwXd$UJLr!^xV)8&tI124i z@~FeZ)yDSpzg50c?tQ?}rJkUc;I+qeaW`*AmahQMyVQD)YIS*E?EURHuA!r)RZ*fn z$lSnWP3{FaKhqy@Qqd5)huLkYCZiXM=9R=1$cHfzX^AjZ6`_Qv^!v0W<_aLE)ght6 z9chf-<1UCl|L9SE|I3d8C5M7O0m7^P)XUT+!oYTN^E8|Okt|>C$2kk-5eylgjWD&h zC_D8QD+e=%Y&=>g=B>x?EQx3tH-e61obaJQnlhEy!f4-&S~udM7v(fZV$cck2R-WgKhzY|;&9ch~?V1t8tumAK2Xyz&z{Y(G|GzTr%wx~CRdUo)5C;CuX?vZ|3t7aGpyy`jO^Ftihp~M}G`YGprzq|MJUH-V59lA5 zQ#~ACO0v8D^}r4LBtC(%07O{JQ!=QbbJZh5(C(L@q}hwCxPSnfVv@7H+gIlwjnM!! zv(8lmw8_mEOd?BiR?y#`V z+K}NU#2lWjXgz00H<`en-8#zTDhh^Q13*OZ@o|MzNFfR|ql|qA^^|n`m1T_IcR!hC zN}wP(M$?t=$APbww2H%O)RS+Wcnw?V-`MzyUhSG8jO~z)v6Tj&XO*~%{td&l{>~Fx zjGi-N)5*n4n>+5;fV)-Df5QJF#d*aS!gpweMB?DaFI=zz`&2^ZP7^0MQf6gS>` z925gbOj+-%uHPALfKr)v1fXg{ruN#KNvjIIA!@Vtit)s8Skl=!Kmm+N`&9CRZB{rQ zXnhNK_=la8?l|83&ZMK&Quws=2TS>cVVNErZPluk2#2WgoIq`TKY&G|<(j*@vNe!7 z)}7R>Dv+uecbby%`SbK@s-(L`I9$g{O23vw{Y;Ka!WBEo=YzELo9Sg-Y1M3#U<|9# zjO=KhZHT87@L(KB3&5P}<6q<#lK;c&0WC~rPQT&5UF3Bc)t#QLD=L@>Az7IpuHfIgJUq%7G3^i11hU%2+15JOHXYS zjJYEpoS~1AOar)?o!%0`(cI^ATszR~k6E|iK-x3&m!|V(C;r|1%F{~iNx4b+3nWz* zl^b1of1$?ZOx{OC{kbf*@kh2TjLoJau1NtbXrA`LX8|+&I;y>n)KeNNKP80GO3va^ zJE0pY4M{v2)K4Yhk zNbEA6L60re3fnwbU)XX=HmdQ4h%OJ7}uPM zKaRrpyE^qW-QG=h;wOp(LtVafUHWbSo;Lt5LI8Mwz@r}>t~{^TH*Dk1JxIU3`xiEU zAIy6b(cBuC?40HP*4?xO{#$lzJQDDtzZCj>IQiSo8PMT&eoS#AwC}z*XmXvE5gRmG zIY!1j&F)_nbCHYE5|of0g7h=OWAJJ+%cS=XrC!7dmdP6r6hIz9pt2aG?!ZI4ywQ2! zPB)q-bv|Y+D-eD#cP+>*j$2YATiY1Lo>BCD1XX9!dxa)(q(M_=kTJUAY-9Gnoy#ed z|M+^0F9_{gzmH)`J3CKy+{P;A$zjG>EFxV>$T&Du8_A7Raq1;ukf)b>?b2p76K-f5 zKh<7ub8es}&csT1hX`U-Y5XE@c!w++$VNcMpSril``!RVX*}G83jZoVMf7*cMFZ;n z3;BT4<@$f|A3&{b=O2HfS!uL6o4T@zVX zRVFvB|{!G#&9fDloxAj-vj_^B@U_ep)A}d};CYbFAEF zzx^bPd5m0`n4j5FRo$zxNIeZ%tHI_O$EV>2y29+vBvp>9w;Zt-^9( z>bktMeQl~E)GZ_YcB+>;jFDi>t%H{&p^e9I94d4Egwh7*5bk&fuR1xoC{kCjel9td zA-yCV$5ehLBrcJ_>wR6X&_o5phm-V@pfmhkOR&*qbQz58@30odsxV$f)cWgXyn2$% z_%-pBRl9+Va%f!Y1GznQelO(|HxbuNgJH&dA)2KQN16jyFeQIvhzvu(p~EN)y_=W? z$0emwLzKrX6Sno^$ruSTrXV{qdd9aQH4GfhclSBwek`<$jcQOWa3jg5k9`;UTq227 z7B0~fSxo1GMEtgC&*LfaHfF%=xg{h3szlL=2m0t0naq!t=&E2zS>Jzq{py!P^_>Ec zgN!`1aYG8Y2`&Mi6HYlYUy0{wb;=;-?`VP6ZD1u^rtELfsbDhB+#*vnX z>?e4=AHXz%C6~DpQC%Y65!3C#fh3q%zI!EsqfBk5^P?qNJ)qQ|d5;(KdXOG`+CTcR z8{N7UyKpAU3(NtWzuN7Wl8B!_iVU3mO^BXFrWIKTwQ_#SvISMM@NKyKqKwc+9XYw4 zz);y{ud){HP2PGDI8n#nxH@vtruNq2Sfjq2CeM5pJM(>H9GmKb7xqO|$34W{ClRX% zZi2zKbiVyZXd)yOq{kbqM@C)e;>I%I0|G3;vx-g9OlLLRt%$ZTA%!7#t-s4eE~2X` ze`IF=8WUCo`+@?e=MN5r33XXEI zYsV%!4+1IkRyZLRBg`-i9V1{?>B6Iy+UwBiCe*E}6dP;er_<>)h}cW^Hiaw2E7Sw4 zA3M!~9hjDkuOxV-$|S>y(xiRAqXyFng$k$`=Qe5QyKuNO4tU^8zS`l>q7bqLt`kQp zDNlG~NV$9p%pM%h-pc~Dn@oCiJM0!B&br9$o3W$myPXt>tME0m zli#CT_wvvrf>v_qWR2N`HOoCabL}e3U*t{3m%*~d?L{h~8M$K3X-HCB)`7*=Xs5i0 zZutM^{@zWY0WU3ow}lFUe)@YHQByv!b{e$Xf$G2}B@Z9DF`d@vJ^RK7x|id3Cwp?g z{y6kWFhyMmHA;J>qHq&(F%8J>n}e$Fsj7H$Ysgs*b&@D)3@jTo#Ou}(eOn?8$cuVB zP&=WG>X2NKoQVY3!@0zT0C5Ik1KH@+>K{-SGrqfkx+Ku!{)@}PTScl2-bAsLq83@B z7X8?I_H7<|!9cbkkL|wn2k|&`AX0Av@%EUnic)<4;r#%~cN4+X0e#Xdij1wG528oN zSDm9+Uc=nMtvT^aJ#ipt|HSsGHh~Vw6V`X?vM349ZYfl6o2fD@en2e=HvBizYg*Gj zdd=gd)JMFGE@dR~qaD7vSevgylJW#l$=+pU`lX)F;ACv7O*^<2Wlji{xOI9u~c^kI5?BjpF>2 zlu7;83qfMyQa`2G*puiOxlF&;NV3;uLn=#+Q$a|{%#m+vUOb1lLrCfw=8|Q=D%XmQ zKBV;t^lTQ=)*h4E*+2n8Srwmr8o}-@ni{CF^hi<nVbVi;( zc8~VSz{Ni-BqJrKmp|QEU7aY*KZN;vZh&w7uc_vzk{y>%p1T$u+xI+q0C#=wF@)qj z8_+6kNIm8=sTxiQxeBmZ>s`oSW~CW+2CLW*iJ)xpAeovFV#aaJ2>TW+##87F`SF1t z{Ylt!$1#4}E;vJL;pllQYNeQr>%2_h1=zd{6OlqWIVIQS4pUheaf5=Aj*5Iw`cCs7 zR#8e$!qI;-IbSx zU&Bzf4;FV3wtKFI{myHIU&MyQgDBp7ASCO3iYh)jw@NxX^L>I+2N; zgs*p7{j3QSFNR65E+A-hZfZGbI#>oc=W`XvFaTa}R0W`FE#wn^W_Q%^#DX1Pn{guI zsKDo9R(%U*jvv9|^BZx09;NW+@ zLfHqGPlS+2n|vxM#KeW2Z+@@bb`kFk%wW~PDiIj6vh{UvHbrjqh`c)B85%()KJyaf z%7E4B5n}R)thW8Sx;0Y2)TLUxL80#N#KQ6W)f}cNU>HAanII|A-H+J?^lrRaH9jk$ zy;_0_5KmacHe?o#A<(_KzS&{9AmQ~Ow|H8k#Irw2jV9l105na~#5sB3>lz=44*Y8N zUJfjF`7emfOKPr>i+l0K+ufYO=yMeN+V9&dWv zJCWM|FNp8?pBm}{H2+x~a0GjWG{uBub-@woM=3rh_v@E?^vbJO9$sp=JW{9)c2@{x zs~hF&06Ny(AY{pl)UiX@|1tQr>f7WfUjyJNP5K+4ezH z<*|=H@>9r-{TISlegjtTYSl|L`!%LI5 z2@@{?lVDvSplL5NwNED>!}DW#pw=QAKLvkuy$GY;XU%GKXrC`w@gHMO^^OWSMQGWb zIP%E-7(euklxIgFm&4$u?bv$l>y%&xtz{)xe&Inhhd4;kVs?Bt8Ak5pejI=Ndon4? z)hKX85DjhKfmOS&MLL(kb@>OdyE}k%RY+W|B817Iy9#UN5(+aSJkCL`=m$k@34O2n zcf3m{T@5v`QXs)1F(7}CdYznvDe!m77b-A3d3(3fpfrsK%R&I)<5`tsM+g{lRZ)f) zJv|%X@rE!yz7Og2B%HEPD9dkW7h+g5+=~yq{|AwtmW4*Gtfo);b2}qL$j=lm4=fq4YO!j8 zG#CrZlIFt6n12{Y>y(hIOK~~=()uYt+9*Bt1;oGqUW9jl4E~M3ZiKjKV7_A9yUC~j zOT#mCuKfWuXRlP~1jdZZ#K++8-%&qf!o=BO608dZ=}8gjT^nTPmYG*~n=J=21YBO^ z=Z+yUxd+j;*U4ll*P#E5y@tEy9Qghscl_04Li1zd;9eYj@E&*q0oWW)zB1{-p^_iY_#_r4PorEc2np#%_wZ^|W#UieN_b0X z4%-6-WEi+mA@{a4LSL3(FfNW83nw3dC$dTd4;HhsqF#}Smn&O``i8{DpFm{j5;$C8 zAxe#Deyl>&Vp5OH;wcPnf48(%W9F@8L&{(*DU#ILBi}{;_IGMbEcM4*CbZp!EG~|I zUr0f}`g1V{4?m6X-v1~f;h+#LS)fQ&r={r*r>iK)v%rz)vstjlXGO2Gs{V^*88i7@ z62U-^5UnkUB^EJ1e;DDA8{HidTzmDEaMzNE&5a|v>NXMP7K|M~yO zJ<4^5`?2jWAC&tTL2>c4)}Ql%Mvf2lW9qAaEXQA9;~)GJ=AV)0`dz;yPfP|kefDqR zas4djzx6aax4sS5jwrl0eF@IaRj^b}!|HS6$?w?k@w*;I;n)8LmQV`ToL4F=^|#4E z6Cr)B_*y4dOrMjHe<>t{T>U+EI&MO-GJ}P?u8{H7iO6;L${drIDTb~H%X~g%jM|)D zeYR(0j8com9nVWz_qL&4ZkfSxJs~qUp^2A;>EwO^fDzYaoh|=r8Zv+r&C4CuYEI@t zSV%HTac@IkcsU_B)Ud5xO7%$u~xec)3^Nv3NxqR83eMY zQpgLL?7HE{HBx>h8`t)Cga;9S?7wjIuYO6|porN!AH&lhc^FQo4{v?&R`lL{3ue9| zi=oLeL~r~DBp>(%6sK-M=$cVjoHg9>$uPe9OHo+f^a%2=|2C}YfXp58K$B20u&6GE z{q>8Ccuh+BfRrOoCwd+VH@f1Gi2&@s(*O?wTm$gljv}={Z>Qv%-oz`nwSi^-D4V)2b&U#uJX9 zTrMG(jH4(5VtCs{7}~rY3z-4}wi+&rQ2QnGum0L7vskvsD!GOq>Mz1p%wjr`L$Q#R zRcJ<)WwD9_Bfcv7T(YVy6odpeatPUzws40ARa-*S5LcXSopcGVZ0qT4Xrkm!xu&!E zQxhrZ7`jAB7X7g3o7Gm3ln7B}6m+&FV|$UXFOi64Lw={RwF3P{$ccKwZI;ts2-y*W zQAo{cDxM&H$eKzhT|>1PgLn1M30c{SYy$+Gfu8`|-C*d684_=B>pL!CB6Tg6t>t~TE zSn;Rt9lIErUvUZ4o)m?bxfz( z;H4)Qe!|^=HX&CB6E@sB>%ij)J5J@Tm@8T_FUMH6hC_uQPUf8$v?dS|B2_Jx5f)-p zkk#suv;|I~O_8o{jbG*Q2Qj>Lr)sZaB1I~tBHqvDG- z^Pzcz{cr`kMGy+vZAyR`pr6tjz`P!K^H=-wp#(Jm94kP;Ev)C|lXzZ)GwuH1n> zC5p{y&8Szc!r=}fJAYgj0%0}d(!Y+MwFmVSDw9vc?e7Is87VPoxE}I5YI8k3u(>*s zIrMSZyu)yHUuS5s>h)kdadQS8-beboarF2o(%GD9vdZ#oQj^Prn%ZKqh|bO^Hf-Do zzt4|j$BrSMOk&NdAzXXaWpe*{%*ATRS5nw|;7NFMVPRft3we%gV1rfYcM-5rRoao~S`{xt62{->Cb`|*|JKF*ABbq2U&Fem>e zgs{CIKaSt9euS$zlr1GB9{Dl3J()```%hSE)Ic?08hjq)8uYRA2>GiNk($|uAhlT4 z#&)~D98|MwvCBfyNGfi^#QDG^Sg&EI)+W2C!s}cDgyf1rLamM(9p!?nH&s`8s7-0&ky=ANNpLL}bo+KOdETh=%@@fxw5 zsL@N$9+HW-)j8?T09UqdKQggVlyXUg`ggQ0&J!;ojm9nEB#R^p+#PAur3KeN6H*Ri z`YfD&5RBg6we~v0vA+IsQ=lM}YnMtH6w)*BMpjAR6q+P%F9G(4km_t=9L3C{(I1+q zyh-p@MUdOWJ5h=~ECl9@@C^SjEKK{t9Dz%9A;~Hf;fY4jC&#{n$5F0S;Bq;hxBl^X zJeZ!I#{Ku*D@|{~)~!2`Or_zHXL8Gi)mo2Gz=p#70r(tF*aDp>9vnxNn@oBFa5%jp z@ST{u_uo-im^YNoa(()?Gu2<}@Nvw4?^Bq+|4YIRGjK)aV;MFKo8k5CJeMCR}(gbU4 zUr<*0^m%e>N)MvbQN~2UfkTsHoH^B;$bIHF^_1|Q{*sWe5@wTGET(fPWHY!tRK$-C zl`s&Hm96||muoYxpe(jEUCz>-Exl5^6>?f|eZK{-&)aCkRA5#CWmP78Brc{Q8 zphh%0a;0hqNQ_zn8Jpad8t7cF8nJwGQq3)`Yp3U+DOBXXGYiMzi>{G@F9Wb{Zfy=E z?p(y!5ZLw+RP)kShyIT&TKrPR3ruEG-%>n<{xw5*+Z(Ude1q9+Uc)Xdfngk6E*Ap9 z5RM)_f`bS4V*R?c7+SRkQ&aPZC2O#jVi@TxA^Ns!;eDMDhin0b|9Aq~ZyiEe9?az7 zZy|sBplYp1uGpFkwjP$FF97(3*x6(;V-Lw^OA15zl5i4O3WIQGM^Q)=;9Y+;91&@w zMK`{e3~B1BOaFHg?oY|Ql9nID{IJ^{Ldy7jKK}{jE=BH5Jb|O|%D<0W{sr%`{Hl~Q zi?Y)$BJ|+%`Th4AR>=NI%$|I>%Z)(x@g( zECZ$n>op37WuRj^ow#*p)JqU7PG$7D8co~q#VLmdJM=m%aLCG%qhZYhbIvv{`Zf&3V_>nG;1bHUpI&{#VJ$9Gwv-z`MdC%2K(F<(DXtiS;4-os7Qabt{yU`D_J;#TV%W@ zkvZ}=aQD3lcK>?RO3(LI*Lq|t-0pk?XAurE@_^m<;e-Mac zXP+#lG>ls(4&rh7_mI5nRzk(LD50pk1V?cS*Wq>e3(G%XjpY&{ViPFKd|sN^S=>3?KFpjvhJz2@jp^fuF?;GbV$%}{ zb@!?^D_blh;v+43ifW3exRAK1t2MrYcnc! zYW6UC*1t}rb<2IE*dLXW#`L23j=o;pRoF`3I0nn5Q+;~=2tvJE<+9e+*F(2%Sm6v` zg#74#qMSLdni*MLNLxO)Ta{x>!)g?Z=&F;f1BXwdT&}>)K)q+MEWUR-oG1#BF6GnM zvUM9KMvueo3&A18*IrJ+E_Jg>(T+{Ms0{c~e&QfPk=G+veFlZ(3_|kRxOWJjTl-u1 ze($@nH+(JjM6W}%FplBOB#PYVCt#3%;fN^azJiMDv*?+x;oi6XEPmvQ_aG;WqW>(+ z;A#kYFWceFLCKj-+HtLA9qzB)iQ_mbEl@)Y3(^YmTnpj)G3$@xmn$F8xByF16t{Pec}- z+|*#g#QDP{Sg%3kV`Fd#Nug@o&;q>lBtob5v8)Xb#T@XUCJm6|vS`z>eN9^63ScjAQUbM%1ZR1!axq5+O=5Ss#em@%bqi zPG+shROFn)1((+Y<_qJl!^J`khthVO%vrG7Q-j|@?Nvlmr;t>2uf5*TGPDU4bPU}P zAA200P`?VSmiRD0q0(s>GAcnT zkoK9Q#o|`_1-W%S5h0ETH z!&Waoo;ZqKPNzEJm*s|Pa{v5SMHYVaVmB@k!j+drn*(pLyc<7d{W*NV@}t;^H%bLk zbR5v|4K`;)79I)2ANc_6fmI0Z`h<*GVRZ}#b4L5L4tzd}a0Yr%$}J$XC<`7syz8#& zCLsO8#b80^DW>HyVPd&x_3UG!%^;zw2;s4F_YBQM=P_YvIPnZXdOB^xtusy)B3*6^ zIzv9#N?F85k0CL30#2Exqdq4(ofVCvb?dZ{LYoJ*>>QGF2hlNnnQ=Y(QM3b6Hwsw> zxnxYK#ZwPyDxIb$`TEK>pPJGffxb>Als>m6u~{(UJ<&0A84|PmHGBehE;a?J2Tf}6 zsb_Rtg$H)Ek6aP#k21qH^a&{qAT@JHO`aSBO@aMk_oxGXxtLUQa9R7KUDKNpw!yRd zXW{DHiPSUyOP<9ftnNV}JU-)KDw}$`nyL__N~(mBjhpcPAACF3u3n8)x`a%Q=6Ehx ztca&_h=jtpXxGK^e2igDzaK%b6F!?f;|$5*?n1`Zi-l)y$LYWRIoM^5Fn9RlNR53S zL3yl3{X1~lw+YewBPPzf9?D){Fw7Mu-Pyx}VxnBCQh_HtfcPYRn0&f1Kv%bomMg!bBW+^x8M*EtT)t>aOR zR%CG5K+je+Q5uz8F6bk}#LETD9lsCJ;Y;E2cMCyjcQ8|nlw46?uMERBc!Ek+UrrNy zPRRYTE{xwr^5$+un2d8muFJ3u?oQZztC2qRagpyrRwEY)@ro;psnKL8bz)svW76Yl zp#*O@giRaPARO=`l`SD#w4+#cBH;7mO>evr9T7ib;|CDx-J)a6AJ_ur-6pa-i+^5iScV^Ij+m~W$yE04D1<#XS_%@h9tdHhk96Wm28dVUi+M`7|?NFVq^_%{AJylZ|Q)%>(^y+-@@Ji#$U;?8tov9U*Ck^3%6 z+ZA+yIKQl;AjFD+f^BBRb`wp&OqKH*0<|&4#Ze^)B^c3 zlcx@S3%%=KuOS?koxr#bZatqpdbbLafvvZoR7`7IwH;Ch{k~E&`%%m#(Yfw=!zsBD z$hE8tlOg>|U%QS~R|~n~m})m@?sUQ8>O|tnpGEG-zrhw-Ezb&bfpSC(tD(aawP^av zorX63%;j{!>+v9&DPU@$gkr^tu8<8qoe`w1Tj3f006d{CAwS$`V1->)NmUuYr@#9t z_*U;mI+euqN8bRqL*@i|boh+>WGE+URvhlQ2nAOZeYxXUn>?;QR5^MRNWmH+N^WQK zAKVZBx+_qMllasj<=EUkSls^!lxM!CJ;lU(W zuOXzT_QDtJ7lAJ1Kqt5J1CuQ?xafZg$%-(NG$&=5eKa|WCyxjLa2mb?Yrl8j33;Ka8IvJaK&6sqrGpx z;Z5ioTCEA8Gw|ZF*sZvJC<5!iH7I3cG6LI7u9hI>&_`!#|JUIVQWB)YyJ~-y2Tr!s z9%hB;279;EN#gmLYFh&7cZaVFvD1%e8kq3V#X=|w^$*V<6huWKR+)tp=->J_H5D3( zkXNQ2)RNJMDL(O}jxmzG#t1LRX$>@Ehx)b)kq*m3q)onXFNE|3wOB<_anpl7t4dbny?kbTZshr7+$#$zO0qI@`%o>+B6|II;OMvt)%>LVqzY%} zY80oR!PCFE3;BX9XfD1MrTvcsshkizwdl!x_z;%o(p@Z~EIc_bd^H!6+wsf62bly* zilxv0rVo7!!R>EADK;S=NIS{>Sv`Fyrw$=@|BZ6LR>P-E#yaERS*#xIE2!Dfcuyip z6DUpoR(+MvElybg&?!74b4d{4k!w*Y$TPlDka46Y@5w##ta}jX-PSPKrbd)nELqK} zX4iy?Mqm=G*9d6xD8|QBdsP%-a=xo7ev4*8*sJ);xC@WP?dWqE!8}XS^I2H}^R>rW z#T$FdSR1H_pyiH1oq81xtGp9JmN|a6?q+g{5KC7r_<=mr1HitQ)7G>RA8a;i#cfwc@r32M(ldROo9I z_toAiHux%7@0XRbe4m;te#YrggM`wyAQSNT!W)jjPfKq0T4ORcVjgUGp&}bpEsOGiks%v;V=Nt4e;raxo=1y6bL*KvOb%esSzE1p9WVmY_yIwgzBvc(8c#d$7BL2++ZOCB~KZ zg%^eNoCx8g=or2NRrZ4tr!(=w`;fo&-i-64D#lx@kQ}Dr`Aa1C{1!T{`wpC4*Td#< zVB)_%jKhEWhv>LEimn@e2BC}I3h(OOs$C-f{c~T%lh5o&C=@~_oyWjXKYrz>-UH{t zF`WGJhcW%lFKUsl9w0wOAFN--@Q;52rI-*bfz>mJ;>>rjaPONCyqNg{|0k+h8PiJs z3?gWt{`x$T!S~Pqo5{lI%a@qkBSdGn^tD5pW%*{hWkKp^clfX{`iT1A(LBgq%1iCs z2&m=deV3&TOhtv$g zaxE%E7(2)tT?4njYh}!9GUd>e7oT`q$2517GS*N-<;t~~*twKXB00Gq5xEw*ZrgUI zJ`0+hJtEK_hC4V2Z)8Z?t{H(|gT?Nb=P-+fyBS&i5=5^4vYbv~=35_?&)n#|{>NYs z^y_yk>3P^JRg}v`eDYIY#KiO>LP4*FfEXI+#!vqEd*Sf+>XqY9eGMo7@fVRjHKk7O zk;|{c#^3w~D)i&5$@6OWVevlt!7UeUTDggFC4EBjPJ+dFXOZDc<=Ja-;Mz06 z@|Yeeb_%)FG@?V78@ouGMSYxR;-}#4-6prc0*fdU7Zl79^BMw40vtx#hO=$aCplu| zbdsxw7VP-!q*D_*A4nU=cjoN)lAL=wX+_vsMbuvvf+J+8WKnWOD7kXfa4KWPzmGZa z&3P9ls~$MLVR&42cr0bmV)!VX+^3xI31Rz@At6tFC3Mt;bi~q_kYhewz=B+t38vrP zQ^XGsmf@`BQFBLi2U%*CTl9^{-tC7~?%A&9FvgPNE~CcHlNKdY&OjG@(Y06{eMtI+ zebFZ2u>=Ycrw^IMQ}VU~e-|}?)HpR$ZdJ&L^l5llQ~l@wIx{v?TR?*9LV-KAGP4Nu zY%)xoD!f>iWghZ7n$MZg+wKi%kWmf&AnnyQs6h9r`G`W-AXm-yg)DB$Y1rIC*4F+U z@)KV{arQpcN>=o|`G2AJZT}lKX8@(xn40z&7mK!k`c0OYzf;MJRd@G#Jzjn8s_`+D z6VnJ^^bYL!*fWS+dL@brKyLII6y{HLT~T zXM|(4R7@c+q(@UcwW&`mHXE3eFe~--cdU_OSWXMdl6p`J<`q(#oj+DTX~M+$qusNq zi5A1tL5K0UYJXlhiKxi2)$qik1K*x?z$HYh*Y84Sz>A)c58Xb8u6mzN+VM}PTo@Cg zWtHzQf$vDJm{#Yr;|?sqi-_9}TY6C@m3?m*^5Sk{b8AyLnpa&?>^KmuXQLELi{vau(2`rzSf=~Lw zaA@pidhk+UN5?wRZZZ0_q+p@ zvXL*4S8>W{s8uZZ?wk!)mj@o1P!50pZtT174(yj>&t2a_Y;p_^w+D8aaQLr8&6oVR zRIq43!|jnW-rQ5fTjaPVT0%(5Dhm-S)wux8a@1gWLIav&M*e8e#9Id^c21Hxu5P2U zx>eIrv&4k$R8n7)VDApywZ!X^q_hP}5-CKEt5Y>2(rajl7PT9bRWmViK0S?cDJS(J zd2a)x9*p9iONc@FEQG5Ji1GJhk_ zKym(ryt=XBcfJHqzXh>}@6@m5PW=lisZ;Q8`91kdNLjgAOlfehho>y#twKOeSnMf zas{>mGOh|4C5j}!G#8PKQn}?#TA^&F^EtI5J>*d54U!`!YIZyAa_s~XLa3b7R>>-nS`C`UmIwrmA~Y3x z)x>B_%5<)GNcmiC=S6NNi&kXI7MWCy^Smv7d+C0CookVcj~bI=izi-!erj~C&8LKz zbZs=sX{r1$w%9X$4R_;}nG}{LPotJCg9?$kBdRTNcq6L)X=z1-^pYq9guKz&ypWxj zGM_Kdn}+l`$0|)Z982D%T!Ag^5E+{?j=UXfRKuiWwWZ_A*c6bSI|f&vTL?9s(pw#( zLkMpv0jqyK{2PB2+0$P|cIs}}JiR*JmSDF7#dIFeoV3Cjy#XJ%pZ*slpZ>6rswDhd{y>^e>e1?PCD)?4j@oK!U}+ZC z`KlG;isOme(Bzc*qxqu@k&%@mTY3LW;36ashdYdNin}XKm^eR(=ri$J1ShO2H2~GB zPMivve@8qugoN~@OBHku4r@HBd@?R1BPqhc245(wlT%sdIG23Sr>ETmG0s{>2n7A4T3L=o zNEQ1$pPCdRw?Q=mtp~?dM$;D=r1U5p&VY`Ertz#mpU`{vb*x31=D4;9^okI-G(5vU z0jDp5E#Y?W(_c5;(TBdtk*C+ooiG(V^iOTs`^8@993K$DIt~3BRsSgo3Ff1 z$Wj-)Laf$ZdKG%saBF`a1>rI@69(j1vLs~XTwbnW(Re;JtlD)MwqN%qtl4=f)?acp zcD~{5=pI>%g_Nuqg)ChfHYPN?@VipQg0D|I@I@h1`;&IWixv&xuu!sMU&@Lv2+4XT zZkNebh!1}x^CMSI(M4~32S&E-7U@$( zrdSq&Wx>{f%)nMdo5hT=8@(0ud#s36ogx5>GNF{wHM|;YglG+I*$J;dfLJOczpdfs z?h-<7MoN}R%~Hc{vkp8Scc{s*!(&6(>z0Y%33-bS2Meg-cKOb1-l{RSXriP~hSL*K zft!9dY(q0M?-EE%?CN{v4Glu`sQM|7uhjyP<9U zA&_>W-=$wjFExxzRL(eATs-hOUGBl>i*twW;*KpRV4aYsObIxg6yadA!e+A|pD!UbCd1Uzv>{WT=AR#`mBv)@z3U`H0EoL!##ex&oC$JFBcQ7kW_R6CCB z!q?z;-iW}KKbHP3XgTezi$KUa7f^I(AbDpR8fvf^Mq;@K*<_l#INhPT_G$%!<1Rt! zp&C({XL89&lnY4|({m_g7M6XGXtGTV&Ao{W5oRR!3joE;JZjmPI$@GbkhnNiYXsag z=fK{i6$K$xZds(R@m8@tP{FXTA}c>xS;}O+G&RuywKnqv7Of#XY1xRw@d7dxu7+hw zlgmXMRlKRYjMeh{s*o%Gj`P}QCY(5xv!dJY!p!JNjO~9KrD8#ZlNHf{VT^3MSXcfF zIU#29+jsSqv}}?=B}DFkO!_C0iJwOIx*JuPaUyF3T;Vch$^41?5$fHlnv6=h)jPe; z0=Zw3!{qD%#Kxb%@I^l?Cq>Y+E)Bry@*^?vgbI(y;H5f|uZ$3(@0v@=X>#TOLj60X zSQ3^7`a}Ab=4*y!;AV!7kt>y~@qF8{G_GXv!PsG*$f~+7YNeR`{D0ct&_4a6yqf49FYU5g5e7lu~!jtOLtQ?ywHWf;YDuYxUBe(qI1=y za-9+ua~2%SSTQcY588oCLKTGFOwVCa!o}Q$6B!#mKk0zi=R<0C63^WE4Iwx}Hr!nG zS2X$a@V1N8hif5Q#KnOUZtg9Kpp|RXizc{|i%&d7X$F@N6(fbtG9g2m<^yKICrJqP zZd22QQQ?&VO&>FdzpVl!*t4Zh?v@9VL?*~BWET(}xtwRDTiG5fR1XcHQKb3MmqZ0vE>P)1i@tJ;SZ zBUqjRl;clg{_bnw9Qp-xUH&(ioW5Dw@eVw3{7>-6vERjD$K&w0#&CG@o$xw=UwwN9 z5&u3+&HokB`7h|Dtd@}c-+1CGmK0pMA@p^A1GdmQ;a5_hcK3;rZJ+wZDWNVb=+hAB-mJOxY9a_}R@9|$ z-Oq|(e!`^z!_W(lE;w-Kf*oB!A7)P<$DTWHgWKze-6=D@TsNDD!55BT_s#D@qGH1} zp**hbDuGF*pB;EI1W8PoM{M!L18UYYvfw}ajq?J{YV_}+?^XAvH>*>4btSY{OAvwK z@J2Oqb^P=rLQHmxKwrMrgc=jBkTZ)X(7*NVhE}fSTj*aJ6nr!j#!f$kgp4WX3al{1 z#WMSYBsZTJS1nxsws-0dvK0dby32=Ld|WplbPQjv^=d>D$9gc3<@ABuG`Y8GMceLF zU6X!y!{Uq}@yNT7nY<7EH^)%4J%F*v8(^1bvsms!GP@oQTO2lP1#^i@kS?yn&XI4+ ztv!jH+@sywcxeQxvEs%vawV~@ZaT@ANi$abS` zD8!wcCdT)E34yLn2>0*Oz{_pdV#(OyJe8h5VT`F&R~U6@7<6X{s-LBQ@S^%T6BiOp zf@R`a&^M+OpMXmQ$eEfS@+=OLC1SP&60Eyt9e6NiM~934SWcXH@?i~ZszD`XLbBQH zSbyo&=vlWJi-L=Be19YCOWyZZk$0;{YpNp3_c8XBzpN}1LK zGQnG|4#dVE({wiO5dE;0ufZa$?hs}Vd|ilD8NHj|jFJ$pm8pmHh20|rIEP6oKO(}5 zfo;{aw@lEq$o7~!ai8?Xb`8qPAf0Tdc0+}&i~(vo*cVKW-Kg)E5>EO3f{=fv;i1-~ z{b^Hbs=X>se-m@}+=9T)zeHr$Z(w5dtw?3RDFiDl&tC>^M@juB`Di+1ZN=FSs+{C@=|@ ziRU3l2ULw5ZLOPAUV8E%!8(wz;no=^d^VZ%>~sirU}1a|sf9WHX{2`mk?ww^WyM^s zR`H{QMf7^AD3zD4TQ?mfXONydgy`yP3}@DcCVZ~?o$}iXVxx~BG;p!*AZtX6v}~vl z35gInpsVTV$km45RD&Qfsme7c_k9IjYpz#wgqlLyPO@sMg`>8HJ1_gTzZ;cCHBWN? z^tnmR>_>d;F%0Z@H@M@g-SLW82fH(`P4T za?h1Y68^>Q=w5p};18l!VxGhH)J4Z~wS<@~pu)X7)Wp`jM%EaoQ~SOKhbx56_0mq6 zIjLHE$h(qkPeeDdkW@v6E^E!=P3Mh(ZYps_*QmDC#D#m>#L6IhyeEEkt}!p)b+}Pl+$&{#8CFL?&b4W5$!%C29*y8n zg3_`~1ajSKDT~PPrO3oiqL7}^#Z-Hqb!v5KCSH`eaTb1gCbpBcGMXoBtNrY!Ho0TJ@4(_FV4oN1Iwpwsg zv@8=!4W(xlDS8u_;{`k)5tbTa2{-E=)O9c914GTekLMF3r7TO<$1VVk=<@z?=De91Gx4u7w{Mdu26y}gV_{VT` z?L=_hPotVmAd|bT-WAX94^k}t-|I>v;!mQ(mqJ($hmD%xv-Dl9A`1^cY`$S+PyQ<^ z*-;@}QkRzZi4YSZS0q2wzG@!Lh8)7r0%-{PKe>a0@J2?kaO(R)vMq-uvOju(lZvPW5&_i5~<*p-jWb3 zAj8~)`XM94*ASP#xu|4lhCY8qcOWiZk^TpqB$)~((7i#mPpz2bsR5(2KX+9!IHX>) zO{N20+v;$mAS>HaE~y$P^)G3Ll+>YG61fbwf~&c!lrv)Rv;)#f4p#t)>AhMG^D97OyNO4*q9y^gPz z!6NMwM&b0QQHUKwaND2AiU*iq{9TkP)390n@}1|(tJbU{m`g&a(wI+m!rB>VR$mw^^gvRiadIZ+|hV*p|nPY!zXt5Z$xTTOAuv&EiMSnyt*jj2H=3*+J zK*z}C8mb{ZdsuzFbi!#BTYztS%*{>L*QH2rvnj ziL=5L?9(s-6j(1cVR4FKEGx#uqT~8|`-+-8dah_eLWopMUWeTkd~m3MO~JD69DH`Y zFDo{?T$?-Y7<7=qMl~?!v-GP8)P=eyGA!@y$Sj-yBXKw8*Y$FsBe#?>e+-@wb1ND_ zDO&+UizO@nt_^bk0o9rq0;Kg?6PndXtQZh9y#r&NUDvhU*tWBGY};y_Hb!G-$4O(` zX>8kOW81cEwLz17`?{a^`w8pB#8`8VgLAP*Ua@kRzU$?~P^hdg9E`3}x?U#}Kh~j# z#|iCYBqB@h#@B${(FvG#OipoZ)@k+v+fFXAqE{bia&EyWPsIX!o@;ln!J?aly%@D4 z9ImQg?ZM?FUFJGnYS)(@hA@G@+tEGU?|<*s;QU})gg_!%h~fVs^T?BWVp;9UkbtwUj-`1lki!D(p4Gwv|7{2beGML{{3 z_D8Ra@cnrpM_1CrcNt4BZ+J5?9cIgL!i-ZzUHt!hwtdy7uS(SK&I(#t%;}&o$LeJ` zhVwyRskXqW5Blv*z5X}2$kRIw6sMuFZ+>Q$KBo(!0T&mWc?@mKO5N;62y^+`fqOJE zr1;4l&8!-&*uZGKqo^9?haxt*R>a{!`4JE%*4G$~M2n~~1fL?AT}#BdtyCsdn{obG z;z?HWyZRCJ@5&u>>@r@na5N+TenNgTm1!=lCQ*g8JdQ*)FdzJhX|y`NnW*6fzoz;b zs^W^-$3zCe6KDLOc`aYM_r6MA(vj*-9B~8ha*Vw9M4NI4#_@!r?htoQBVP+Veu(4e(H0eTMQ?ntPdfaI}tk`krT_klH+EUYITz?ibw{l z$qJp6r?=C@PMa-$HDmD$C}UW%YN?fxnO;%u76urpKa-3S$HD;w&len}R0$;f`5i$% z4&nYF&ixm832H`w`n6~($OqqLZPvKiE$DKT%+%6--P>&0Vsi zjqf8fN(W=~eoz9SRDMnE(eq3LJcKXh$QcXk8ZLLKFlMxQS@@e`XV7a z%a@x;h0G*T`4(vCces1&0zxbVmox}8Y{uPOaLoMuw57kueID)A8V|4AiC-ocXp?dS z6)nzB{zj~%51UpI?f4$+IUwZ&+jX4WTXiZN)}0;JWNHp4EH+6$TQ**e%G->PQ2Sc{ zoW${cpwSNH1S~Dl8CbU5pj_cBtlcIPDXp4c0jN9mhIuLh>_1_^CfH_KRBBKYZ@>LVswvmgs9vUdR&yR zaxV&g3?=&>!evi)W8;3h)QN%UiOu)xCF+0>Pd2z7_7BkDWX4V@SxN16eXm38CHa6lk38>0mdKwX^Uc(<^)*So{Hjs4 za@AC56;CB>vfuHE!ep~1B|UQnk}9e_IMlC9hHtFAYbsF+^mr+38vBk?al{Ep^qG2J z4x*xq!b`wXTAI*EpuH+w#XtYsU?}yi>Zn>+y#N zjs4Xhj8zcC(!J-(J3YP}!1=jQC0g+0zS|MLJ<*y~37@1T{fnO4W^*|UHoHc-|Wm7JN1 z5T#S~I+hASK%ODe*+Kdj0K*&1V>C=HQ6S;;RLE@g>3a8fYsN)$?|W7ZO^aJs)Tv?gR+sm&9URdZ2w9i#jZRrvRX+#|Me&<1N{KmIrQg>&w;yldwlmIJ z6`vg~`Dw9*z>hOhoCCGM|>5MTo1I}OuSF6H|tCid7 zdw3kS0@+K;bsN%8TS+OO+V%G9g6kn*aI1XwpimzK#-Mluy;*sIdMqANN2)-Xm5kDj zI-JorcTvTGd$F$0KmJGawUb1oVqmV(weiE>tFpggKH6|xzi~5(#w>hQa~eH@-98Kd zT_t!j_V3{*P*;*?cR*=WHlgJ_gfVK8{0?&1xK%wxQ3Rgnjx6Vw-lgb)ffg-(s?R*K zX$A%;qe3eUPB-d5p@k`X^k^jbm`kt3Pg^MbUKeOBX!d?Q3DZRl^iuzI`&0j*0w=3T z$5Gn>f@;8Lb%m{kF0xZenOLcg3SQh4(|cBv#U>)Z#kER2ob;vQzov?@%p6&Cj(ban zNt&XM;!P)WkqD1ip`4Vtrk+MRJM+1XU}Z#_17Q;~s)latpANu3-Kg!9wPd1UbDq4! ze^V&i(Oh}WJj8yFCR{_2G$5$0iAxm!iC^C}tf!5lXYDa-Ie(4b5a9?OXqJXQcXjEu zHFcxRwj{|x-Le#2SO?F}(Odnf_Y9xS5CNkY<6m34CEm>m1L9j9A^5BkzwanjYf@az zp_?);C@AbSln=C0u-E;-L!a1?q7PockfL&i9}BY!^*gu$*1c2Q|4sAOz;VJ(5&pCP zcxfh{k`p>Xy12ubuhZOrH3`^dt{|gK&R8TZy|+u6P}+l*-{H!s7pIG;`-GcYxsy_( zPjsTPvG66eShP_j^QueXdSB;jDNBU>`@~aq@BKk46&;>yWD~CVH1QSMKLHC}e_*#_st%ls&jAOKnf~-c*!S!$K@zD;BCK~hLI3&0 ztG?UI62B7%Az<%@jMuU!7g)@akTM(R&nsy$z4qICFlW414LJ`R=j!nFs zX7!f}fZ{}>g7jxpl%eP6iWJz0B)J=GmyXVOETy*C2L`C2L$riSm zVo@;W<|PomFYdLDpies$4h&ZQNuUEy;Y`WamZ0vEmJoS_kmvykv&`m+Fc`W15k5$$(Xo5*?WaCLvNgo-wKZE z_Qw$;GZPcHE9AOi)hhna_nm?QONmQbg);HJk>TJ0N|7iTq|7BUw>O~<-$H3N{1TX0 zOH%~E*$1U|Rrz$=I?5=0M6_XJv?+y9tZ3S(vreb*Q!eMltvg~a<6P1~k4*}`LD>U-8_(LN?c*qR| z5yE2D>0oVq%DZ{O)vHq^(#`o_7H+y>h2m$%?BSFvSii9N3aPh23nw5sFoJ*b zR7s6m6!~d76KZ3UXo?Q_PRi=ZY?rHp{uX=>IzR*{jTQhpk3n5CKPN-{*Ye>Pjup`fyGI1}3t?u)A$~j!5r6LHcdq2m+XTYHw7- zK6FP*zmei6(EMR@gNluc`@W%mEgLDxpd~`i5eMg(Ib)WC2 z7thGAg>JxIk5q3OU)Xkb(JOVMV^q^`-lW}3Jy2ejU2zP@1J(+h4zat9Q*l7#(eXck zyZGq}zd{m285lIDaym6cZRieeLI|-;fVem)-N#=bd#jIhP)Vc!DWPRWA_rwFfZ!Z# zQxEe6{26LFwhc(o9`4gyW@bRoU=aowfV)&#rAszFNH z7D5owCldga4{qCc0{c$?&SHIV091+0fM~r(tI-a-L8npMkq`j&uK1b$8;_p23M(#z zdEIB?_KLAI%zjb!*3Z92NU^;g$0sp*Ma0;O{=wM-YVGp8+;>g7)$7NOLt8}(nl9Km zqj?T0zc{s#CI{!UDI++CPB0U}fXm=VL6HpKi{6khd0f(kvc+IaaG86?CIopm956}Z9Zf3m;*y;HGMI9#dA$7HJr`L-|RzqAyvd1~=J&4akF+7B|5 zLC4L;zD0!b-iF2{l_5gi`=9KpQ2d{02A^&yLd{z0&rTBd5>rM_p%QFyPk9fYWAAM1 zI`@p@5mzQeSxj4%UkkUAE8}mjNk+!S|A8uM;qjjvwWbLfHdmkOT{f3ZnF32AVs!99BXS|Z=&%`f zKhQ#x%Er$HD__pibY#}+wqvKn&dGxlQI|fhW59Jfec)^#B)BZcoG0VtL255ReoUln zVHC-RX*P!B{zeA5@rfTY<`qx!{*A(CDr%T^8rGO0j4(|px(GC^(L-K-xE(UBi3*sRUq4bj!UWOgsKJ||0auLf_`QtPv^ zGRXrtN(oC^E}P@z*AM*bArT_`W-y^ zBiM+prCA5d1kXyTFyyUcdBx4KMI}$(5BqqJ)RRp2eFXvHH4Z%=K`R)Y&dP&y)l~za zW2IKn_zLnqIa*(&UqK@Gx;${YvZ8XX604S-x_+oX9B6`_7UjWJ5`xcUpbc3Ru|tq? z^=70Q#-|Lq+F820>{n)MsW9XQn|0^XJ zV3lHv5VLyQhJF|4iQ?@K=Vc}T4h=Q$TDEH}Ii_R@bcISgi4#`g#A`-YflFwqs zOgCtNi1Zf

R#rM*LtJ#@kt*t(4&fSwTWJ2JjIFNn`LBaN?M+ag2!T&?UQTo+0B1RU;IC6QD;FOzhG{OHm?OvcNmvEBpWQ8|9jntN~{_NTJOZs-%**!(b)-99wZt`xbSms<3N@= z-C&Sa#$!amC!9|azKpu8M&nZ)zok~_%fWA6dOIFCSeQF>(u)7f&tAlr6FU`dYLrRaI*TStmienZl6At0}}^N5BxlLbAql2 z)aSB@{}92~^Yoo$>coe0(=B(ss%a%`^X`~`XDt0aFK$LiRbtJn!)-=RTTaXovH254 z28f|(iHRls)USmfHl}8KeOj&=HrWvR8(byu!ek+wg7JX=hJUZ4sBex%nX)8E9`)kI zOyaHf0|Wni-TQfdYJlADVw;Gpj8G)M=oZrl+qUAD&%z{}P7zSXBVV=jw759dR1XG{ zZN;5o*fo+u%`8j5D8uuWYb>dyVeP5sA9RBnG6FUmo1`^@;bBxSTsOO*i(*#))_l*U z_SMJtfu1B;BbLELac;~AHdCmNUe3rgfamXI=|3hBHJl&=@VE-xW3~2QUtlCGQb|MO zO3TEOGrE%c68u#lyh;v|?bLH(UGI=!rt+cv3#x}RtvikvzNM*y+&}2s?3|$Oyv$@1dG%Wl!98i`G@%srJM~HAuxQ zJR-AW#j~Un3aeNg-Z6RA*9&K*XwP(4_YV-MT;t8ki30@^Qe!n&@bipN=fsIHp8qOD zu*d`ovY_d29X}d=97<&5&hB*e&1P_EF~2Y1ew(1z);RC0nL20PbjW0cV<%xnmbCfD zwgd_L=9UkXtc`E5$|#Ir<^Th+7Ab+?7O>gjDvbI)`M1L5J3GJTr#~!MhY1UPl{mL+ z&y3Apl1Hxe^UutUfu;C9I8Ihw;LSepFw$yEUzFOb30qi?l%E)qta+~8p(Q&I)jO6 z$4SDmQhzr~`E+-b(#PDB*oij0!8LK&PP&=87p=yD`O{y1BmcG*ZF?5els_I9iP#(0 z1gDt&)qfEO{gl0$uahJkX-;X1RP3fCQ`vkCMZt}`Lp4aDK}5o5BLg@+5t?N6gj&$~ z%!*6u-q>Uyw_g847(I~6XOK9T3+XM&G*U9ER;k@871SWAm@-6F>wP0>botpQ;En{D z`XQpd#CrZV{sf1Iq)V$n`b_HD$=c^W*Su6ALH7L!&m%G#RmGR4HkK zW-hgEDS^)sr1SFPC$sQJ(l#7dr#kEe*wR<8v=l8*mw`gi$>Y5$0tw+iA-Mu9FVH2a za5zL7+5~6Knu;$#JcNNkvqndlavVmrdeAy35oZ>hU+50xxK!X^rHj#X`Mlvgxw~ZD z=m`6yHVn{|;84H8%f61omm70`$^ihmj6F6~fO+cr`jN$|W6XPPE32wv=b(47hUqwz zfa@tRlzN_Q;_pZy`B7-Z^Xh+VwXRoTu5V95NPoi6@;jGWylINMZ*A!WzXZAa-k zeHpD3U35;AlT|5oskE55k|I5x^bntH6F{pe)gD&(P9m-Zn#4%DqQS4lq$t9f$TQz@ z`(^ewnUK)G)uAEx&Hp)(IPVRCbBlM{n`Lv-?kXp;l_-gEI(>e8y78AkH`?$2U*N3} z<;WMAMLXWMjy8-xR5MNf_vN1+5tp}kW4Yrrn(i7b240|yF~)i6jiyPrJiS_1cA9`I z&3HTXe|%!aPzHz$nzPO6EZ;|#d{!IxR$`chiPGa{Z;+f~i-J&rO8HeB_s@_zQ4#KP z2`y-T30}@^GG&FUPr@|LPV09?TC?@GhimQG%gY6P>+&_IaKtq#fT!asLV4n{^%<+7 zrHlKh&+-ljs=Cdi*gJ=ygX4j11EY-4WrOADkl9^58kk-i-H4JPAfZaE&V>6Ag=*v1 zx)?;~yN>opMDax7b}B1xih1{En5{SD%O;S2c}GjcdSAS6{QLqub`E#Cv2U0~T&bSD zUEZvHt(+pO>zoi0>XP~kV86~HihmLz5Or$O55JR)SqOTd(A#IeK~H*MlMJi<4w06_ z2OTCrOV+%BHc=++`6n(k#KDuoCASyc0r#6x|MJ}D*j=CBC!dmNXs zhVno@Syx{5%t_qE9P;3Y;p|FaoK!}sdv}+iFF$#ggXGju2L4-BQSe_nlc0+YCB3p~ zmuG|P${In!oPaB`#WsD;4o(3na8gd;La+yLRSSQwFtcHBpTRXf{zcbrEzAimA!53R z{8)D8=J=@W^3#q*GEd-Lyhh34XJ4sobS6)bqwair7G2J~^T?tk*y}Qlx9i>}E3TEx z1<`&8RUyNdsR4bV?0sS8R6MVI7L5h!HI|J-RO1*cmoX2N!e4Gnh^vl{BjsLG>sDvC!LT5}T|ABS1fqp)TQ#xjd z7fV2$?W)i%E?0&#N;M3oVv5(P_sd}RBA+pPV!S$AuXrBnrNDt-zAC39p0n_9gH7IsGN(PIDn>fbkM9yLB;-~ zqetwnzw(pPmmD`MKDc}9kiSk#P?w-R+NddNT%41aiI5@dDJQ`I5IQ4}p#F>AkrWJp zTL)K*V-04B2lGWz`8hup(^Qz`haMP_#nQBSYX(QUC`EN-6v|KvwIlC*ano`Ja4ci27_OJ5oA;@;}(?Ag~i{~2+d%BoqnZZ3Wom)8abU1x6O?j1M^87Dqja8RR9qV zpUmlewuHa?uC1DvgG+Q$(dS?kjfqd!z#h=RS;yo68qO+79x31oEK`c|FDb=alFMyI zljtW)Dr7PdbND>_mH(m2TljO5q(~l@Smt($1w1qF<-HMZU-GCokIrUegsGX4|F80Y zC02Wo&DPb)u1fY*;FM<*^hb0_r~NTTgpXOECNnYaDkd9OFH!SUhTRa3Y!K=Vf^2_I zyPmX8W~JoI`-R+0g_=rcx0GUfl!%M?GcdT-G_8dGC@`a2=ZeSz{?P;HoRh>#NJkeX zbk8u;1z9ih@KTJ6Naagw6RAc*PNDd!*Pa8rhMJeSxLPHQw!h8nzx2(lIUO=;%puR~ zuYSMm<_&vV&2-1QI1rrmUxg{I%-!bpx?tY(gR^}j=7q(b)-apArrDu`GojnQ6-Tn@B^=h@>e)+8*C`y7}I%6 zm|F!5=B2Hw=k>T?OBIxY)dJ%(4-;K7ki0ig6d6U=s|4nluZSg)hdtQFyUDjj-K<_j zm+(Lq=6Brpj7ObqGVI_N?9OOs>Or5q#euqVdtCC4~>UbX|EQ|8AWyeiV2VES_ z&47iKavc$7C!H4?TF1nbUtn?&+(@@QoXlq1DR_)m%H|)!RbfiBk*yoU{CzYB0)KN2 z_;}uQ__p6-bTz`u|Np~5X?~_SDbsAbD(ZGJyTyC-OZ-kOr%JTf^T(cF_+3DCXH~p# z5k zNh9uyFPnurW|jV!xjjU^Qk3XmzE?C{mw#N;#qgtjtGEs&2+s;=oe{%1XFq_hvj12? z7g(YQlII-J?~oa5%rkLPPGsS*feJ}k+Agw+gx3407r|$VeBaF3GLK%@AyN=iLy>Spk?Ne0Z&`wxQ3Wc)ph$N1OR)X!`xcxu5h~ zn}+!n(egj(3B#e;CN;U5TGH-q&@9;YcFFC`a+3J^!?=Ays6u_9UT+jY^OJvta6RD< zB6Tps7?9U^GvzvF5e5=wpB)Wg;XTEiqp}BzOZbx@3FWeo#8aWeyG6c>8o~Or zTvYdCZ{9nIQd3T$NJIljz_Fp%ObwMP<6#xkbbae`_MY`FJmcf=02$&Q%f~3Ny!c*Y z+SyE`>km(GC!0<@v$k56AoVJ7PnzNdFMxiZF0G$qe;v6XdBgzmM;cJhR*}_h7qdHa z=hNaPm>Wb$$@TfhzkBbptaxJUJB%~@(8maEXJ5Jr#brsYU&*R^l;$oKm%a{XpGS5C zi&e}_#6Ed-{RJG>*+gLaB~tV0vDj&vj{F0Yv97?Et#S&R?CG^yzCz1=N7BN4G3{7? z^S#~`?|HKGQZvUxskzATSHk{IC($Jv%T3n?e__iT{0^_|y>Ep<4KeVmypKGJlgAwI zn`O7yH7Td7N40_C46L+KE6ASSc}EJBXB&D>4Q{BEimv zVh9=xft@)b_=NrNrkRo1>{wzwq5pl(46#-4uB8gzS?(JL_`a@|(2nVp=?*D`#MiSr zHOrw&d+9?5=Z)9|-QFQi^Vtv>o~U$Q$Gf()Cs^mAHkZ>nsK|jsyT#Ezx7)9&ljfb* zxfx(Gx-YNM!LzdJ*Fz|NrjjrRPJLxB5#?PD(xK>jE-{aA7xhl>eKrsZ#*>~7JS9Aw zvtRabjDdsxeeB@g5rcHItKlFLdg4r(Jt=K9Xm)o<&X)+FwRk-|Lbh^;L%3R8Ua3(d z!dufthzsQML(cyoA+`i_Cyq-)5_dFhRc?-XWH+izZRn|=QGrq?p4y!FnP>`~xc_(B z$3u==w<;;Rj)ucvtSglSt=ACzA1|qRnPH~n9Dc+w82;}YJIp;S@F&^;4jDakc4UZ! ztnc_lrKv2UZB)7DdFcbKh0arGy(E{?X2lw3lo8!Z|J{^O$Mr>R|H^=G2<4;}cW)O+ zihV^MmD_7-c;8b!c@_7BRR(NUNk}|7xG$2~{aMvJ-S~$~U_pv=Jr5YhTSs0t**pBZ zI9OrjLAYpHai%vsyI(F1UJb}Q-blWu0InUmr;+)ff1L8W^Og{C?l`jz8^qkaA%azl zv8Uzpz>=#6oROqQp_>jhhJlUtnvZ~p6(Bh27!Sqci5XX`Ep9HQq{&-^DCoKZXxh`z zDKUnD$Y--1Hb#tzTp@FVs<|NooZ;ZvYURnvv9n}ABqzdj6uGHQt>MgZC=7tcsROUhn!ByZ(IOnR!r~q(TE5yizUpN}_x3OVjM0 z=9*u^sg@RSCSeZ908^BZ#4!obo91ma;Lp&64U7PnljoYM3X~9Xk(7zz84vSHUC;Vw5XA_FUEv0E z@2N|fn$Sq0*SvkMd7*u2pS}=$%uE+>^W4c^+s1R56T=Sdio&K!NT5~TO0kjFz#f^7 z+L6DcWR}fp80_sdaAE{5l9s{ySIXg7Br@7q?Iue@2yvI>m_HN}fpN`#RXFIx zCeyT6zxM|xaA`{S_!xqjgsL9zfMU>=Q^SLHTAB_a?qM&1K4+7tJ>*gGeLnrs9c6@! zAT(0E%Od-N*T(irdBUR?;Y1nu%nSDwW}2;KPE@*v@Pg0@u_)r$su=;kHCaEg$;wXm z{Dx;Do`q*xt6|*4&Q#ZX5H0%Lahs2s_pLIumRTpwu6nh30YXTtWMZ6wLS2iTCeC5O zfWvF%P_Q7&)R)96T}WRggTxF`?s-k1b`@VMbJhUbc?1l=o#3s~1x&Zq&s0agVeag_$w3yMo<2G0CapV|>S~ z(1?YVw$u2ixmvly$Y$Xx1Q+#N)JYh8BIMt2H^+x{1h{{;h_l9EJYn8Jf0#6Y9R%kc zEs@LV&R~4&|4wXChqX3VTskz^FK(?Y`j^S7HVt~s&}EKZFZ+f*drF0G@9m&M3pnrW zfJ`mib4xB}nIx64Cl0!2!mN)obn1=mJhSY%&D6$Dq(RWE=KxixLUTVY*=x;`m@6Q9 zoMcX^vNU;9kyQ;@pwa&6elp7gs7TgT^BM=9*r=< zgYq`W*?c&oCM{YzHm$W3hcdRegS6>Y~OA3!@+Fd8h$ckxDPwQT_kUh={d+ZFM zl|YchMIzAZasjrpj{Ma@w#~YI0^Fk$2}6FnA_U=8vXU?n%N^Cd5w*_^T}%Fn*SY;; z2$Yh}fabS=V^#h%R=p4pc*9Aw(p++qkvv@W$Zd|)BrmQd?mT66o*pndrCiKTmY0vc zLdt*gAWAb#xAoP!geWSR4z~brfR_DgIOdW-g35lyq|r5Cy;}NVXsfG~mP>+D&7u4j z!3Q?;#R+?rBjh2MxF8p{D5}-3StS6+O!~O&wL^-d1cYfjR(Y8 znajQrEQN*dlU-xE3=4ma!W_!Y8Ca+E8_<};C0>!Gq%+9-gH9Pwt|Ii3Sw52?*~P?L zroWnWh0IJfpN7$aEyagxnH42-rS>g-^h;KNEC0Y>J!lTf>=LsO2quikI zLR9BN;xYcYu{3J>?WwByzoi5AkAHCH|6Z`n|BbbpU5c0onzF=KA&Eyj3fyyEM{A_w zlbPuX&|+1wZ#$>y0WdZq251@6(DTPq#$<(sAk@dgm#(tFw^gt??h$Ei5)(2Gs~7~r zZFK@MrymNtyw@pPsw@hONaai~FsoUr=&}2kQL2c8<7Yo{jXm>R^Oo8ZVt`yesNJs@ z!HpmN%?}%QNVZU6q`RwHReM@x7WD`TTzOfW>CO4YoUBpbQN=dyZVE?3{5i^ z#Qo338&;(XZ0$@?IXlwYA^BDSEKY62V`nNg127l@x~QNHwcvM5TQyCtg6TFiMp#Ma z?}UEFY+O$GzYE0wq~&2FKZlWJ-aXNnb$TuacPmq15>Q|-`S&4E;^dW( zL`FkaO&w(G&bWz!IzUv8l1TH3VU}j6eLT6`Mkg3p^j-X*f85X6N^R9jt7Zhkcuxef z(gAw?%+H{vXG1It8+cp;kCol#aP~g>10RT}R;A0)#7U$)$Gi(|_>|EIZV4saz9#D4 zKvtN{Rxumr-0KY#ER4dJz-A;VqYr%Sb6_)4kJ5_;nm!dAGO$4pHEXf31>foou3lOsM1U=}Zma@xPyj5?Dj zXBJWNjn9KkaRslLOOI3mb13xJSd9w1kwv)7J`f8F0(d65PJ(jfipiQNF&z&98>6k!j%2RQI_={S{`LabhP-D`=bWWv(bFYYAuObb z7zH63$+bBH%6iPYiTLh{WP}Fjq~#zuPwo|#_1iIM5(Z*-yF)MX%un#oyir*NAn}Dh zidLUA+y4#BHW={>yjeI4;9G_Wd!KqaJQMC(uk&MNd*7|N{6n@&bhx>oT-}fy$B0oh za;C&0CZ*VX_&rPSS;7B(bf{K`?>SvUnk=l+iLX;FOoy5K;q0<^uqZAf^7JrEBV6|1 zzNHJx7fgbtkev9vXlgvCSL0)&7 zBB+K{b(lD=IOWD@`>)K{@c{nvelsmZDpv-(9pfPiG5|)VNdlkquZK8WnP1ud#SMCl z@Z)WNzW-M~*!MDAPOi%GhG*5aM;1*?(lmbg=HGPw>sK}~AtkKUoa8u5$(%dS?W9eK z_#lFd8`umR|GPbHbudazojVf3CDbnjim&_R|dEgSA4>EBc^ZWL-UzySzIs z&D0nw9Qhh=kLb8!%G99kd5{hWk{{~xb#w;sI)^B?eMw`TdrE+JWMA=Bzn2%3KLj<- zDmnMAREoJ0=w5YxnmBwPHV}c3ffYbjfnHdj3D?3Q?~{0>hotydcO)FZvg3;jb}EXK znYc-tZhPXr?_runq=~s>uTe}w&>*;BZ~&pKnTCuhLrhX_V+p7wevd)94k~pOnmVb{ z)-e@HB*XW)t9J z*+$y`2A5oTc)%`jdriyDwf_DD{A&W>qYFM*3PK$_9-u_{x$Id-;Kq!x!dE2e{a7{| z>KSfWpMDEMczoCu43P-yVUS5E^_+^9_)Z78cC3IZq%5cV|5)A4f z7^gk+DogFCTAzgMe|R*p+5|U#|9c1mlwVN`)8r#~wHsmVx0oquf}vOhfT@9^`o&|Z z(EPQN1Ml{TdwpVJR7^j>F300C)Qy-_egx_LkijNz4$zn9$$z7qFJ=7Hg8_qnPU=Qqo5?r_azF5bzhw4@(>nuxaBA> zcsq#)CS;Cub`4YTV32-pV-TE)1?CuZ{qt3>b=KyDSCJV}n^D^PsXq>1=Pq!wji8iZ zC)IYNxat%0bFq+jBu%}5_-n-Q8fn`qOfQa3)-4sDzm{Zsl>7M_XU>*E03sTiHOD>w z6URGa@;qdiBV9F0&~tMbiB{v|Ip!Vvw!wc{wubM}GMRZM*t<;!iv{T?HTs!M&uYH< zoD+o*PgHpmg!DcpyA+f-E(%d6qUnB22B@uwdqPsu_Hr<>KU!tYwNl2|yBQiD>DFP# zj+Y@7UDj-mi$`qKG*q?A0bRomZcl!AJ+u-qd*-tS$IuVCwZqtQ=PY^6w**L3`a?!2 zL@$%kYA1v+TX}#ra@CiS8-E}|ov;p27^Yg--W>1iSmp)t(Fmb&Vt8mgEIQ+9Lx;aQ zgINX|gIUawF)C$6Lznwo%`(tz|$LLpnq44R^QVX`95%?9j zT5w~(3Jj!m5;wwaVAR6J5&t3})k(ixur_1Ot7SqCGIfPpLBL{&-wuN&hf~mtL&r^p zLNa6?S>(f~)E@N21mGNNp?1e!1T<4+b;K(Mf5kv|a-8UUPd%s@`WlOVmMnKDhu-m$ zM1j=N8tcmyrKLqI9uzTP9JrHxA(Dw7z|orz(0b$f^_#+JtRqkxX?RF3Y8Gv}@;fSl zCtFw`Xy~|Fey{0#^oXm^WlgY*$Z53P>aeU<@MmkKLe2R9z6eE!bG%spsrc5~;52w+ zv-yZ;^s&5L;+|mzxZIx2C2{om$P5^MfufC|X8ag1g*hDNtN;Q+J1q73h~pxS9{+B& zEP7yp`mPgcrmt8u5!bDm94bt;WX3&dzCxl z{{q6tAmiY-XQQ&HtPE6^S%4$$Y2 zSFZZ&sCL#=9q?CuR;E?o*_HPeG}i;hC>?!GTcNKtD`?>|N!C2J_pBL* zo^+C&w1T-csEnuh1PHhQ2n_3toFcc+T)pO8yA-V*4i7F!*e*TMXoBN};HaX#%9kjj z*okTF$IMp#fU3K?D@^5xO{M9v0i10g1KU;f-%Q zy%8(=;@qS`ruB#kT*dhDc@_o=bbY9eHVqHpfsh__GCt<4VZ45YN7T5y{otOg*rDrk zEW-8>6w$FjR=CQ>gzcRW$~X7;I=x$`S!c=UA_tz!ZvK2BBipG!qpx`n*8*5iG<%gT z_(!{cSjWPVNrO3NVH>v?I2CleLm=N?>t8m-%w&~ZrqdAcu0I61(uR8F^8sj%`g3F0y8t%SrW6Ct#NsRNY<89lWXmTT8oY?wf?z1>BCqv zRN!{e(fa^SnV8=LC+l&d?fieS$x6%eC~Jl$f)l;(cO0z0(on=cP z2Y#E|Su<%&YK7xyRtyL)8m+vAX3ANTYyRJ}f!j%;1p%WOHeYA(H91iK2g^r1&K81kU!Oh4I!AK%K{ zUtCDuVvihlO<`$fj_-mj5>E*_bpQ(EZ94yZf*zwyzo$wms5nMDVBn|T=(7)!iz9;5 zLc-OBvj??i2qeFT8F6Li^#=za``le+NuXnOtR9V!@j_0Ca>vve2Tz&f9QT%g*sbc4 z0@ZM_lOY+o1#~~;FtZSaux`^_czI#>iow4Iys9{qp=&a5DARJN`}>@_*1>?U-r95A zs0Z!2MZopD9V^=&u@iA?Srfo;FC3V8{PZwR^|?ZdgT zIaCna`B5(s(8GNSY0eE#jp{HYN{sALQ2(2eo_emlA-J0nvG3jSrir-`WQ+9k1P1c_ zO6up(rNeBxeKpvb&hv7I)ILC<9Tpok=OEVLU^`-pcWfN zP!D13@&7_+342O8%IFF=+|l7+^VWE}`9L3g;|)=$M-wtkS5Q~@j|DcGoLZ__Qo%hW zwjkWPc(#jo5{k(?q6sJeuyM2m^Jmfb9%GcvWB`9^n79G_IDc0x^XT$XZwcL>p}jVd zmqA)y{L{`5e!Ur4x5Pa=pnz-qKR4xrPy}0v--qt{lgUjeNC}%iu>N8g!BG-b)np{d z+G0%ft&`DME8fc*0db8uk7aR(?9?iM3~m@dlq4>!g=d9DsiErN!zY%u>Ms&y3a;go z2D&?HAG_Y7$}4>v+LPjy94UYs%eF#`T{J)xnPa-QV!zA20{pmS{jKo)!c6gTEY1Ep zqWE(fI#)GdLZ6bI65(pOetPYy&gTdPrc0GZxvnR??E|*|6-nm8!cw??UW?X69Zje; z+8XIU#j6&&s{8BVd?Frakf}#tVT21PkEqn`lQ={{!?M9$N@UJU!%`oUUwVc+sh_)! zJSp3FB`;p%yt7h9Q?6q_O_#2n7(7>>p<#<&GSkj%+w z??=SVhuC>R!WSDiL=Y@A8-3fGli*={T#*+iT|b-=>(U@W^ZT{4G7@ib&y}anXqfa@a@&1D70y}IsbnEe?Wl0+2(q% z)<;Q4g)bbN21R?kvl}c5xV~J$rN}16iKt}3*$9|$&FP>rm@F>g4`sG_Aln0_ax6OZ znQ|;cK0Srh_$d*Sjx!=$>5&fi)8IXab7xX`e7hY!hYcYwjc*f?u9-lh+#Ebat-MR+ z3NbdNW9Q*+z_E+Qz9RA!YRDju5&G0f1j_)c)qvViIi=3Eh6Rh{uB7wSsS}8to)ok# zd~Oo4&M=_TLwKteE$>-%>um8Nu%Zf*$pp`m0GvTLEFK$8FpE61m+>nP&U0v{&dKED ze5ww+ujyyyJbfluwsBK?~rmeTbsb%pfklHES|Nq# z@ZnNUH6kh;G?aP5Gvj6)OBms&37DPP3)w6Y09pmxIS*T!)mn(CMpRife5t-5Xn;aI zi~Uth^c|BuDBcTN3VJpPRiXDR(ie!lahxhnm^*azheDg>s#;cxC_X1Lp$jb-M>=P- zou{gUh~d?VPR=ls5T5RTlj_AU0%)qj0eY_Ga3M0VmxQ~O?zL)0>vJ{Mj-39*j&R=_ zf@s-1oc?8Qb;xrg$B3FZ`;vI~;F7y&PMxNEU4?S6-xm>~_}B>|8RtqQl@=AV2K!hM z(X==!iLeY%pIR;0$ii-zxHx#>!(kgDeY=p)L`0~O*`#J-&6M3zz}KH`#Hpbs_`L&2 z70*!*Q(!YJfd$n>jPl6mbv$sV3%6{(vTzKq?k(cGFC9YO_A&`vCTi?-V1wUqH!Q{) zq_Syj=&<1n4|+-$lyS{qUIZ)JJ%xeo-5B3LNXHQJ5n7$+RTQ%-2{9uH3z9}W*Szfs zTE2)PNfZNpBw1xhHVMHEH3)60N7cr9)NE>m*|D&J??!g@A<{WTM9wM%=LZ_}`o)}r zx?9`eZt%{WEaOU%8a)OF3BpnaegkB&o6n3NO*(OO=SzqVc9Vc`l90~A9SCC8tsjDJ zbHl2nh+xqRW_Zo)7eZq|IJE1+2g^i_mna&M zA$jwKx&m5UMJUQlJRg50J+p|H|Hq%9vZV>D@4Fk;$|iba9zb+KA&NpW39~JL#KcJ? zNElYH{0LoJ5Q1g4;9X<+bQrnhD7@9{X#JzGgqrzuvBBCis|Y=ioHzq#XgR%)G$%pK zK1&MKKWi&|9A}BjaJ2+FmxJSzu{o0S`!PvjE!276rrDsBhR^5##J?klj+B;+&D0a1Xro*9lXlh`V(q zpb9s-bc~Hi_>1q@kk!s0C%=ZGPUDLRmKi<-jQ7K&yOAkm@&9}&fQIVJhZ^A_^{pQ# zaH6w7^TsJ;F#ri(I1Q^HBS<4VjciWGZ-3f{Wvz=C@U@Kh1hXbs`D70LFLk2piDMXf zwT~vrA`M1O5Go@r0gN(<5xS-D8@R~aplhYiH6mIp9CVe07R^aawyYI4v_7~J&G#%r z?Up85UC&=_`tz?oiSD1An)&M*0JYd%-~0qxKD>P9L>X5Kj_#f4-z&DhvzfYm$BvwRg`VM3j!1#l4~hks0WQcKzf4z4&W5HT=25FI%p zG*`Z+twi3&X-r;49}w1>xT-LA>Sn&GL3r7$d*@1mDp8O)b1#b?ys1UjU%w8RP_S*2xXK7rzo`hzI z3u2{$7ErJvX#6OYybIN#B7XOaewZ&7!0v5GWi)*KJ7L5UMwpEjnh#iOT_R?hA&Y!o zLrqY^@BUf<=JP@M%D855#RRL6&g1m=4&&(m-i<_Gl!TKGvr~qV!(em2m=#TX7iO;K zdlqvl)GP@;y0@dwja9$38B0ID2F@yX>AQ2l?mvDJC;sm~-WNY}=7S5J$ZR21z~@i= z9(=78GiS@VQsm;J&@(Zbgablbz+hr@C?a8d(gU|yC&E&I*=ncH6`G`Th$=?h=2x(- zqQJ%iXvsln@j=+?w^H9FL8K8$(@K4dE#1#enz1J;!C@i+On)oXx7h){+N$G2l?v({ zM6OgLt&aKo21wp&?EaJgf`9xk{|#((NYl9l4NDf&yw8pe2d&qw$NC38jO!kH5EaYT zB0Ii^I)e+6)kngRV?E8TZ?i)$JDEp^4iI^}zqDIzv7M36gUZIv+?m)Q!o6>xYT13l z%*yeSW()TD;T?&|iPNa=_z2ya$brapcXhxa0iuKZMO1fo=$@;oQ~2*m7EJcMO5~tf zIBYLWB>l^P?r$QJ7#Z9LyRW$vWq+9VMoy7n@ zm&g^z+{(pA;A_}4YqHMc)a26f(-&=cb`KGB*Ne!Z4@OAB|H^B;5q3ixqRBKq_fZe7 zUtjXcx*Xz3CRh<9iD>ayr`o2oyzJrw$YeEq>Em9kTd|NKOUih^;EGb^48GotpFQ{} z&OLD)O0odSVTNR9ht7o^G<-kSfJqKnOgM#D4udau$xTa=>rh^X-vRQ4_FqA4{EN6h7 z77c14F9bF3FqDltZmZPbpuR~o1SWeW-iX=o>XeQ#)dAU{!zb%>FJ2`W(B{z|{huOCazW`v0B1nFSitwQb1 zG;m~e3JeVXBxymF) zz=3Qz+P*4?1dGktA508Iuupn#&e_OmUswn)F1QR%vFnbtR_q{NROUhI=sGr8Guc#V z5yQI@Swp(mPVS-~P+2&37>6_AjVvXye&yw0|4D<{fc+#?y&(Ln%x;SyK+MAz8-i?+pfl0)*sp71&k)Xh{|JgC^eR`^(D^Gj z9Znt_!O-?zanGwEKNcIsLJ_OJd>!l+&Y3f1TnVy~e#o*5oKxgR?uN3g2b99HtSvQFJh&ezAspjh8M?xH!}g`bTh`XIy%!POyr0Z5y`Tf*$2pg zIwDye4mzAgEOQkRB$7ou3~I5|0pH#YjaDDVckaN@(c`db8hphfYUrpYlI1nakR=iz zGy@yyvlPAe@qNc}?7Kfha-z2GuoS(vR(XO&0A6IfQ*eC63*tb z-AC6K#IqI;I?OiqVLK%JWrFRu3rBrnlDgn~Ugn@W;kUIoDi9q$2%D=~XftLjU}pG{3CDVL zcpX+)P)`J_7DoE5hzznh6`%d62Msl|^OdTql(6SW76s~(Tm?pwL%BS?H;J$>KkR|i zesxT!Wn6t+lwjrJS-kjbkK@cg9}yZWHrdhK_?|#G%-bZPJsaBIgWeZA;agq-PlJEv z8<)n^$uSJRcn$_O5iY9mxC&sIk}sm;H?E_VziZ}X8JB}d)S8-xZ1a|UmfqD5G~ED) zMMteg!#N^M;ertblKHFMMciGf5;>yp6X`k;H(~pf34U6QdYv{ntg@KO`_npIXGFWB zM6whjX58JF({s{iK6JjvyFmiNWV6CR?^NvVr$2QhQ)$HLBPLWq5>}1$vy#Y`jb>zx zmfD@&39pZ8{*iN3qFZ_T}JRgnnAH?mun9R7``_$`0Lg~Qrhr* z9nBl@Q)xYPZ8gjn3^^-ga=u51QnVhHv*nN$NksxFzcRTz3D4E3>CsZGj&4;O;SB2k? zzv+6YIiiyDeHLF&WRW9=M@Nnd6OqjoS~RT*Z**W%$RQ2fq2;39<|@?Ri4s}lFb8bz z%qKanS1Ub>`s_T*!I!`NU>`gs8%8cMN(5*XaU$0EQ3EXm=mC5`yRQMs$xbom9Kj`G zGlO{ef`NU$9Q2G+g3t+#-#kd7Mq}4isiUn*NB^h+;e-*{yaCC)grF;nuYKAAn{948 z{p>`tI@&GRwAv<)o33-f@42+xD&vEXUm{pVrHE%g`51<56mt%94#FP-g*a`zw{Y2efD8UB+#c0 z97co)l|)B_s-l_7U@=RG&`$%MQ)sbq=)fWD{l>T8^IFlc^>(4nDy9>qK7X+?6ER>y z!ah;)(PP5IQqnXmkgz=ux+e?6d};#mks}DU-y!HoX&e2O*VDzoK17nuLqCa>j-P@j z)FFOfc+3d(2@>kEQv^&cIfXBfnTOkn%EEq3$*D6iTijxY;e1V=yvc&oL2)~9^XdjO zk?KyE?LiUsT;~lR+?Qs)KOczRqia+pGS!tEV?BFdaW$64AGh~{Jcq+F*pQ?C!~tfR zB=T;~`9dQ?6|B~9eTR-EwK`TbX}E5ghWoat@K`2bw1sBAIX6fq8n<#OAaNPjK7MId zIPh0H(evynIG9}VsN~UdIdEs;gTAYg@MjZNW-K9g1M(&-HLZ$6_@HDIq{os%3!@R4 z6VDI=K-}qyz~&G31(V%YO!SOm^5p2uCl?0$Yl+R0S5lS_1USXTjIT`@mxYoThitDD z{wxn>dzydNo#jZUff--Zr?Jy~5N`R#*!K+?>zjCP{)vmLb!1 z0XqNGqmSbcI@Y3R_scMOY9V_nN+jz-Jr)>eh!-k7d!wZYfb2fuWo|~n#ts+L{1h8L>s7WIF$|OJLG$poWd6vR4+{6nDnxl zlYPC|=j%%5{Uu1m-D$i=eAMKXx>O=IHV2nM&G#l-#*aZh$?=P(eWvtQ^nXdtmB ztdPp#HwOPBTs4}Ka-HtNWz20WGU<80p$ z&h7XaP4LveNuVo*JeF>~1+_~%kWF*YQQ%|M1vEJ5GrGoXq7!d#cKVY|hpZX9ENE!5 z6L@6kfCwewZCDRoGEqO`9cxJp|M15+{a^nalczfcsWO@+oFt;vsi-Ir+2ay(w4P7= z-oM62|KyKha`>UfhUobha)762*()&7^#a{nLDh=;iD-rC{QUbY6Ddw!JaYU=v2CBz z*%Y!7aem>!Oc84&5y~EdwmP`q<&;Mv2RSUrUS`$%a%sO61xKhreOw(=y7qINl;y zy)T`?llOcR?rK)6mpb}I!JR*&iow(ITN|m)TzFz~Ihq z?EjPPFkdPz7rnDWL~!GOxET$%v>_koNnqx)xkB8tfSRq1a8;LcS6vF3>2AnorWmvs zpjA7ISZRjv3Cv(Hp#;WU#Vr0XJiBl_r?B;DI1>1ePO;g{Jzh5Hiwca`q_%OHVH z6MBJ24m%3_8#b2&7CR%;eI>V-CVcie%Aoh}{|-n0;7_5(6W~mS6S9oMBy1)M1+n8W zPUlv)x8f_`{TAvr-3%==MGKU)(8l}%Z2q!PJ*gNy@f0dsZ-&iVH|M~aZ-GcC6CO@b zor@2ncGbs9WcPA&%v?eDW$gwByBs_H3_?rq79l9+8|+e$K@#fGp+h2izJ&v5E^^#3 zYHi3?FDAQRh0)@MuXa84hp1?elI4@h6#t}tOV!DHeZ+Y>HxnAFq`0RK^97E}W`xP8 z(tOwnSH)7u7Tyu3(!q3RaUl9EOi*POo+b2RDOhQ4SMnN6^j=mbdGxYe zo?MD(B8?4N55uT)xE?ML-HbKH#UfI=8R=9fEGCWWWHsF*CCadv5G0jWMKY1>#B?vo zyLD7937Nc(0xh0-ry$pTGLePPXMx2+FHczH*DB+Ej<*QbPaphe^gnkNwhGRrb&1K3 zI*bx8z)B?J?ngg?hMU@EK3F&ul6jo|!C@TvUvD7UABSX@XoBI{gC0*S8Qw+6X<|O| z(=X6OGS8Li9MS*tE3oqv#PyhuWbxS}zk`aE)iYOv{vC#O;-oK8x}(uByn1siM{R=bMu(NC601tLRlZ&Lo9^9;T*C1H2c2DhXk z9gV_lp_Q!1NB7SO0yjncdA%!#k2W%6Q_!0*;rR&}Y0bp(M`^HWaFS5qcv;I`8kReY zBr}NEsU^+YJ5kbLga_V)#Sw(FvX#hAif%IVW;{meev*rbmh46H)PC%JR|BkNBpb(y*@x<>s1G=Y~$VhZ>FO_c)b*n!i za?A0%%nj#Bn}X4kKSO29jUvw-lb^+vJS8pGRQGnOvpA|d9)yyPTy&gX7F^F34-wrn zf~a|GH&7YoNj$FrJb#q(>m?8#JU}FSEA=Cp`cVOj!UUW(Iovjek^?)DfKs+2x{lhK zn@u4?!X`GA6<;PhlxP=ZBH-TYc3RYC=t|!B!`CbxibWz!CMTj3oybrfhHCGoc~K>T zM7)X0Fp(=j4`}F3neDaC`n^#GwuPTwTGOj%cf)(wZ#-km7O_S10>YIyz$xUuLi@>TN zk()2Vy~w-Rm}s4O;s|#C$Csd{3Xt72S<_6BkLK|C&VPqMJFnN@8}z(*8bANilS1<* z0+?P1G{NO#Sv>U0mr;LX>&(eAt_IRHncM6>5tx>V4QsBt(@c2GLYrhZ5P_j)Q;7K} zOtj28HhYTLc_PDF7za{Q zJ;=s~5op>%f}P`2o&Wr8V4_7YY4+8VfX(8}pZyt*|LI?#n9mDs);I~%ee^R?D2UWM ztaEwji;v*5-}*L51uK+DKb@g5KO<#1tOGkEb3QqD^(uJkHlR48HJLrA97NbAPCY}f zsiA7wy->15bmq)-t{facitBs$_>T~1*bHa&8mb;5i*qOk-;3jFad1>ua0RuE4U3vq zh?hn^Zib6qQ}W?DoLIpgYBRP7IqGcHGl$dOrbAbx5WjwiF&>+m8|rJ_(N zqu;0?Ditw2o)?kN{XQc7G^Z8{RCXF;b|N)OMu$cVn@rG*MjAt3?yJF#ayv5KERifT zEHV?EnP)Bn_}*L&6TJhy75?};cGwWX$8OP(ED#CKCFzwVgr^mZML9sWmFActCZlHL z;lPq+Bf9&tM6#GXnu%yh7@N!@rP}Zxf3pJSOUT_aKDZDB>+}x};gv5u2}|JOep5^$ zIM65iTYYNJZ_~s(Ka=2WkQ|QTwg31W`k(4VHkHOr|KmPv{(o&7bEq5(+f&eyl-4Q0c|U z!~~u-Y@^k<36@+G1E-He%@ym_>bnUpJh}?!#bgWy` zf#3VtlkhYw5q?>^@Ghq3lQcG4y&`a9cyOOE6$*hbe1B#Kb^A=)B*PM8KcjD?cr*jOpK=yjpX4q7ceoFK{#cD zgXWV{!w#4VG2FP$jCjTjkK2kT_WSXf`~7IDU&zfEWxQV?;$WTlm;IPNJW8ugT18)^ zld{ny7f$2u$3BVL>zZcHUL9DdBQAMxHI#ya)MyfSfAe8TmbaNU-UFm3lj!>X(UMyJ z66$GoZg2m@D)`!1)Rl2HkdF?+=B@^(VBys{r@^!4M~M#DPR@)D7G18tx9N2+9RI;m47* z7bj8@Dru$KweK|ycb!5$ox;>mFRgU52-Gx)=h|t78!3|T&Lq*M4+|2-gpdhXZ1e~b z1P|QRYlWHXvI3nx*XiDRAr5OzI~qU#YjC<{bicfVUS2~xl_P1fAWXmesN6jhlQ{Uj z$FcVI8{uhQ$-%;ee+$1CYiq2&CL&pV!l~PAA)-ok$&S&pIXF^$Z1|u^g6s?}hnkI2 zdn_(7c?(otc6_$^n~;h06IsrPv_MQ0xs0DVA*6G<3Im}?_ZsACf_MZ$2TV#o|mV)W23-02u9kbp8W4Sj49n3zIg zaGJh51#dDA+vz@N4b?D&0x;5i%Hu=qAY7#4@VK$T1O-KW)^!XA%xQe=R6RaF9>gco zRS3Ll!r1o;xWVJWoh$t?PRTg*>;!UWa~MCKNB7AbqDcj{jaH#FRmiIjV2-@%Om^oR-Vk0$v3J!NI^_MhFj+)rX$GHNZ95aKJ0&E%-=ZRQJ zW(!i$Y2-3#*l1PlaJ!L6XOU2VEj|_Zgy%w56C_^PO#7T#})vLw(%&8nEvmlJaCw_up>&-;eN`8HdB@lVX8k?hd*I(Rh zL*j5RDxQBH>4)#c(WDPbB8Qd+8}$_lW3=d9+G@e{bOBRiMTCM@Xcp?1Zt9Di;w7db zmC}U}cLqFIdEiYLsSg@|`{QuYiorh^Pm%|+}_sYoD z=HUrC@dy8X1o1=$|M~X=IM=1)^zk%~ADc#BZyZ%Y3rtooEOs3Y%|2}1D8ubHqq)+9 zjX@1xtUrl35t)}V>ydX6!6LCBQ8^a67{sqcnwvr9j5)sw8tp zbWVlrjN zzkS#OUu6ZXHZhTuagD&p37yq=o>tD6FaglmV@*u!11sJ$a>d|)q3;KB#W0yU={1c! z9(|VrO~M>{qKvBtTScpw@Yt7#SD)-*&P1rfLX#wWo6|W-CP75PrEBkNIQq)ZaAN24 zIQ+s-Fmmn;oOT<$wAOetD&e(h>hDCV*b2-o%{vV(1|3V>D%N-u)Y*t!(WEYR1F}gn z+DkL;AatV)$znleLo1P$JPGJLy^k5}7Ze^DBq2(~oCKUo@8g$f?CCTHiGXpkWi=az z#Z@I@AEhR{=v_HL!c)0C z{@eW@NB^6zLGspdcP*L3COU<4y;*jmm`{uMn(e{muzITKo<m9KmD9d($34F*abdwNEo&wu)I5G@7{#aht5E2&Vz?#;#bLk?e-=e+ zy@ap*S`~Kf%H!udiSkPtTHC#7ZS%owbJNhZBAqhg&AkciKSGNjn}XeECh+ZNa(HNW zHTvvPx_1Qj1g9%nyzV8LiWrR=vA^4iRgJ7sj$t~hA)2?LwxR%^HA4##6?JtMtX*Tq zncfU;xw{4*f4Bj8c%evUEL$BwORFkUYfX%(NTp?XXc0C#ZN_uEcr(K6$$uH|Zy1Ye zQ4HoQnJ_}@>Slf`LqvzCpO+iYT&+b;qh7{UgV4Cp!CDxTBn$GxPVP$`#S$rI0ZIRL z?R_2n#}5diC0ne>XHw`rypIHGRyeWyC7lSC8OMoev6+!a2)DPQ8AOaLCR^fnn$-;? z%=4)T6e4!3YU^oW<&n!3(6D?B8dt3sj>*|XjEIK(dWEKkeU6m*glQ0cKMN<7BTr>ea#5A5%Z8uUF&1dC&Gp=ib{)@fvkjI224Mb?)?W0M6VV>%8Wi6a(H zdh^)TvSI-`kJL zcm*2j{csx7D8y8livVN|3}#=8v5DB&%$;N`}%5SFud5H4eWVBecUK2F5M zA_Nz=(^7|q08NxdlZ;F}im9PK*jygSR-4c)@vcCQ&@Ra`Ejo(Av7L8k9Zwhqnc{Ff zZ^v6rPrvYUlGEn^XDyaixKM4*BbuX0UoWC<0Gqdg-hv&V=f@zJa55?O)~!Q)?6}Zu&8bZ;0e(d`4f3WL#cf08)x&VS`UbLh zZG%+rLvnNsgQHmz?mBV>12Rb?`bShOZMLJf!GeZX2Y&W=3Sa+=QRLDF5t}JY-wF7Q zXsS0Nlg?whKZ>LNR>bqP$k@3LSt8jQwTDV_U~csD!eHMk6K~p}#pSovVQf>qxVtTg zC0m?GCUxxH8%2eW$fCm9Uln)X?!k&B7X0|h2|WMo6i%FoBNXBtz$!Y=#cNitwrlN}x#5wWotZdjgxSI*-o5#gP$PvcZ? z3`dWo@cL05^~)-8(<(a*stI3zEP~1L#9|MRGOi+w>@3a)hd;Pj$K2$aRBBT+OWMuLErBc&U^9n}6J!grjtC3(pHc9N1Q+~B7L2@#x$ zl!9b74_`w&nm65oie(!RRV5OT3a%#sb6-#q{fKuXz5{fv$alv?M^liA4bd26Dm44q zmjjAw*{n#pQ*qN>xb4-Kkny--Ws*goTP(65S>qFv_-{Agg0a)1aMZ4%ep`xH#n+ce z%N!uek%l9{dHb@+B`2sHw#$y2IfL6nb|B4~J4uG!TTf(Y{y7iX%*heRS*s^moQMt| z66t>ysvJymxNUj9Fa}&O9qB~fp4XAMeie4+LWuOo#HNbcN+S|O27K>3X(Gi%96OvP z;$g#Ye#L`7{KGmt@W~3)R9oRTQlClZkVqtviW6Cv%+P6JF*&SaGNxi;!+J=^&mp|G z7dcvBuojp7jOJHMC6r2{!>k(ddTtu$ENR>mslpTyke$1x@K^tH3_ttvs7SHaTu+M| zdasjRIfSarIC3b0|NiIw01?0~t8COajac2`#)fq!oIa62HYMZO(KNbF$6>F~pmbSq z`%V}B*3peui%Gb1GV~I^o)@?|INd8J)}Nfv@ce!!+N$$N#bU5KCHN{#h=dDRwQ3bA zYZ@^T%i@iL8PwJoP}Nd_0+oqlW`!!uSa)3$Zn$$bHf;^!#w`|%M;v(JWW!n^7U>s-MMt; z&>W#xV)ZEyJn$1^t=Vcje#&sYUAAxN1kpLkfL+uf=Xc$NX zld4Rs{(9J5ZW6+NRJAQd>$)wlyFJKfGbB(;qK4;bO*devvk14$DM#KVf=KXk8k@w} z3D|v2Fx$LDBnk*vbu`;FaCT0W`hcnyAkk{rg#y}aD(`g0BecO%b`QeWsXR>G}0yQj2tUkeXrhwBgzK+cw zzYmV8R+wxa7>Q6At!|nh=qq`yn^}@AdS~_{V~s$TwMxDQp%GdP7))kbERrCcJ_~nn zxgdw7XI-#{X9s$JCYrK6Km_X`^_3bb7pE0t7l8TQp9MNQwdZU^)(>?-fBY4M*R`Ri zY8_6$nZVF!8aA(l>(+Xa%@&YX(^$L8371boY&wh45x{0vFfpCL;8+$uj|0!Y5=Aso z#I4)Bq791F+uECbsPY&gE%U=eWTAMh7wL{=VBczPRo*Q()Ai?s61k$ z&S(fv8r6}N1*V*Abneo6$Y zvOy{l6xys`b*KduC;on~NVbAAaoG9aJ!bIdCpBxj!hkYE(BtY66G;!uMOcpKhRo5X@*8optNC=@qF83C>;(vL6 z0&o9bu&9ubC7~{2NzFwH@-_6%S}qP}+bT3Xa4(UpCt*k? z(L}^ylt>mw{_amCF#75nSkqjM@u#0c&p-Ynrgpvp*UD9})zqSxNz$!mL9*z%>x=hH7Cva|t1OO`^LqJAW2;H64L;=Wygm0(al-gNMk~*dQ=9nnl-{ z1a(vycW$b~z)%Kn><=TCHKD#iLQ5TMedsYp2WqPfM7~(414qJcH20rGOtm0F?GbdA z7|Hyiq7$>C>n&4eJUcyx|L*ID@2^dmcrFKfNXN;}C^FeRYN~B$YL#%~jWjM*BUZ0+ z<3soQiJ};B<^=U~ivrnV#*!v0Ufw>8)5o(!Mv6$J^QiJ0;dRKcSk1`M^G3&ItR6Sx z#g1XPY%Xlme8`#D-*S#*k&3`$*leUlbdLeAb$QWI6-9V5im?%e-cUkwlZ5WmQS^>j zAlU+#m@1;O5~!|rqWf3`IU=@q-dcg7fh-<JuqY}w)*!H) zgwd}!m~bBXKYO6%X-c>h;iUSk+)#^#n-@N%P#Kp8?}%duYgu-3qltilRgGSnDR?L+9SEKjPhFEO3*PU2q1M(zEDq zjwJV$dX|kvHPlbDkq*Riiq~$q1=Sr`B2Uz0J zgSAWywuWV>|L~pYd-A7ZXIT}Iri9syjYPKUMn=*9-N$KO*p8{A$1%9?AabYApy_j; zqyA3yK=m~{>BaHKOu{^vj`WB)W#`W?3|QO8X=S2B;zr=9?htLWaQB}9lPpaYL^nEe zRP0c+5^c@4B8WidtbyW^pPyj(9R2I#S9lMNZY11(=nnyB4k5rO4 zHsyvKOQVU}qp)_FSV-^!gcB>zp(vs()U9EOO4t*Q;VbQ3s5x!H=gtP;T`nOO&fwUQ zI99CpBa>i~O=K_RLU=rjLf(LbM|F(!rQnbiB84v8erE+<`S}P=98cn|Z53FzEP#RG zENoH{o3~b?t=)$HK?Q^9EbeLXA=sG4%Z*vw8wtWmeb_(?93yL(XUg*KGV$Mhu=eo$ z@B9_kUKMF>;2^F)+U-Knui&2Rsh-0Iq#_*d!hpSdQZ$wgNMs~>UlkQ@9j(>0NEs;* zarEG>)d@`YrchmDg=E)}GdrkHR-&#(CK6~MvS>uWmWD^lBRx5esc;-8PsNco)Szd~ zh|Mcaf!G<7M`BV;PA3sJN;Svo>or(PehIaIz?$%~Ff5j6GbHe3Q-#!2}Lo1p~ z5iF9{{*F-DwE7;xY^N1%bUq zw|a}X-lx)e{hit-o#SY^kr+Qo?QapjIq%k%h{>7`CTwpV;*-*6R|eLGsU;0uE`dTi z42Q3g1Z18bb>(+?2WGC*h>X}8mZRzOpTg*?+o27Qf%6Si6^kO`J5xBG5%_oB!#LjC zC&*&()_aLyCEiii%Y%uD0@+?cgm;q45{KQ_08N=2G5KW}z;Uh;9xg>IIRQq9nQ@GNXb;3ZcFrb$q@H92u^VjLO~>R_cE$#DZ> zaSfq5Bd%NF$Jk^Zb*&m|8{I@w97H5ER5V#(r?rID4pcYkFjJq7Ml5i7fo03sC(D4x zzL(_`9@x6YhTzH|9EXp?9I!!eZh##nbEk>z=`&TwefT^{c{jLvMHSPR*iqgF2}iXU zKIuD((1;1Y^=chDwz_eIh|?2KC(&GE#tk<)aqmZJF*R5~JZHejNDjMq$8q#{2FYle zh(;d$Ln_=p2}h1jBds`)EfA?CV&8c#EBs$olM$0~ps~@6ib@k2Tb$TvaN!%4epqD( zZr1%!EgDp{6!wBi6r1!R@!(uzaZ-(*t?@c4T!HSMLAp+cFW|tr z{vw)c4QOf1<7gj^DC+nAkQG1PLF0=C)!HR)SgkVFwktR`8Jo&im zy=eW&%9%4|Ty3y9iyf_b54_ma0+)tog7bIh-}#u@vmXr)-ABZX2xX=eDgVmBf~`m-%P-<$ zaY`Q6Xw4-J&pSZx%>H(~(SW1gyQ@1$KrWzZ@Lk|C>qOMq1eu&TB{oFxW`wy$cvl@R z6m}ed;bkD*8(;_Q0yeHKLaKJdKy#S; zL?_hM?T~C1B4tD(Sj$SCptQHaK+i6vmtsP~4*4{cgocbG&~Q2){=AUWV9S{B+j1ug zc?FO6uR@IENz9pnHESS(u4CVsEIa`Vw%+VQ6Op3+{sLN;$@s|qUbHl8SkmD@E=@mI z$cRm+v2%9@4yO<6SJ-fFD1zxUy^ux5vNkLFMpaz5(FwQ5f|qtDQI+!2I8EZ`tD@L; z){mM5m4(|QsY6zcFz1O(WsN9C0jXew%=t#+M9g#a8BCCp^gEqXXzeiCr9n=`F|~Xg zQRHEsbi!XF35|9xl}C=s+1y}3h{)aAjTW5jF5r*XOtiGu6dk7@Jq$~Q6_u-M#Gw0C14nQ_^__#r zMYA-yUt$2z0B3^L{;^dvC(3v)z#5=jbO;vu$^JkLrbA4$SeWp-g0nB#B^@5K4m+_b zCRRm~{j{KcCyn88KqQB?7gkqLq+nS%P0U3=5{-AS@w;<;Cl>C!YjHkchi%DKg%f}9 zJ2>_AZ-c{`a-CcZJTs(7>KaMF*4+0|=t}C+7hDKLrkEUZl>Kacl!!?)m4oW;oi#i= zu($0gzW@NzTS-JgR5^%9baLVh)l0MR!&(T(^GkwAI;YTKAHUep0kKO`?6lOc(0QNx z9UVnULV7d~Ji8ryaV&4E#rExEB=~I@n26KDS4YQk7y8bsXl}6}lQSWn zRMF5zWXNEK*KaCmqyh#^jH}3xn~|S1V(ayGsFDGZhz7sUiiQ>kZoE-KZ55D+(RB_P znLs6^gC~(85|VFjg+(h+pO?TP%Zl`pd1Bl&-9cfq84;yCCt)@lkji+~^WyXz4O9wr=yH^K=Goza4AVo1y1aoIIXI zHltC$pasrS6CQlXFLp_ejhXT3NBp?!Lk_Il=*IY@id*jtVB_@x^qkJpvsJXW5eZQ> z96Xsp%aDwo)iJzWo5yrKkAv1Y4$4WKv}dqOi{pTl#LjFC`;fqCXBux5<9Ib6$8mE~ zeD*s1zN?tT_UIJyt0MT;wo!E3G_<_x#r9oMINa3cGL@Msg|k8|?*L*^8K=6aEt-k& zZFZr`uVYCtOLL7IJ{mO*wHiVJBB41engVw8O~`2F9E<04I6Y1TEK$U#fivgQa8ezw zAVM0;%IKVuaO+y0Yu+Ehwab`21i|88Xib9dAD@JD1u97<6dc*T_ot`P`w~rRK?ht7 zKALcTb(nUZR()(DWTJIBg6VAyp#7mT!FsPzif}$91iUcmysg=oXcg(;pKm|t|96oq zjsovQCO#w_ojvU2tY&9ETtpCgV{*l&uS{$}Xnkznda+x}Xf)6Mpw+VEsln*ek7Msw ze_xmYFR`=|IA0H`zCJX5>SM5kh*)Mzp&YIpTn-f$M_h~=`IS0&8;vmLoEYf_(n$q2mkHTy9?Mp_P#KbO z^l%at6*kCb32vJSekXAHSOTMiDkhT|swXR&8>~c(RHV}dPELUMxP~14-W@Z*qvudJ z-i_g^dSneYdWQn0qeX;MDhjj!iKYxl5wXe?RQg>PHjrxIv!J!zIH5+Nn%20G4+-inMr~!VD1%9`Vss0$IyJMJ|EW)luv9i(t16`Y= zKHXkp!m%L3iunBE;Q5QbBYcpEV7OX* zXt{p{>Thd9)w(*!u4|ihCkJDA;?}=MF{_fWyLe?W`Kr6Q759Dr(=%tvcrTF}I{~M^ zh1ioqviZD6)RjZ*o}**B=T#Awfdhdq=uWjuf%+M1&N!A-CN?OvKj#x#&lVh5Rjv-; z%pd(F4*%D`6$I-_V#V|4M5(a#>wk&%fAjC4L%8mCK) zql-ZVh$Z3~8$JSORX`V~kf4T{eXyHpe!B%9p`hl9vmp2J9Ja1xP7#36!e)>RWFrzVR}=Y;0Bf{MLs znG1jONG%%wM8?5yCb8zjemwLk4+=RIb|M!&eOY+jCR7p`N@rzsccpOnSO#}&rm`fm z_~v5~3=L&*-Stj<=q?}XT5KZc-c8!+^eHPt)3OZKG za9b2ihONkJb|R5Uw6uERHWqPa|0Eih)SysIV=|gSDrSe8ilN4l97 z4xsy~Q#kk3ag4s%4<%Qi$wm`e9?8b5Het}eCcr*eryo5iRN9vU$HDS1525XWvJck# z0Vb;(naM6elsF9xlcU)jnqEX=;)Ea-?A)x*CzZ@>LC;}LkJVd`^z=EzCr*{ZHjsd4 zAF$aRbTgzU`rzNT4lTDmh>;gwqSbf~ms>0I2xj_X){1EO+#^I(3-nI2B&t*nq5bjJ zAvw_{cKxv?iyipc`F%DA*GIVb4H2u$U9}QLI>!X~>VVpvNiX~3rKhM^20EDVQJq|% z&fk5|%hUVjkfe8)3>L_5>_gb^ho!9^&6Osc>n-5;*&L=OvRK_gBrBvMm8G_}rqE8r zV);rNjOHRDL~vX#6G8zAd9{ent}F_Q4Rszf#?Pc-B!Y6bKL?w`iMAFO#>P!J`Z|a_ z0D*cVqICbRULAUC6{0aBO%@YI(pH@4CBhK$Kw}MziGI>tKa!MS4Y*L<=0#GnAZm0# zYOO^qSOvvjiP5%7JZDHF{rxo7wY%`;-wP5~$Rb~4=X)#0CQ=xmP_ccNioJW1a7!9a z?aSlIC&uBeFyXF`IpvQ{6ZzfPo=tNcvM7(5pseU>;b)0NWVSSSao7Er^S{|{uPQ);V4J&DpK%_D{ z8N=%3E}WgLfXzhYDJwy?%2-BZ4MQ;?Re-~zi3IXShX=BuD8g;5SW|ocZ1n4E+2oM)wRL+CNPz6;6K3JEJ%T$OqG`$YnV7F9!vA zycoD%GLtE>A)xWL_L;L~yq6HB8O~2g0^VS;5mO*aF^hw@p*uK$rBR~QzJ=r~xBu)l zFLOgF6oFPqAwF^#fyQk@Ya`pdB7DPa4%rca-P?>@d<@>rThaQ+=MdSs9n%w&f@EE4 zO6SsQi6rVD{s`pC5VY)UHFzaW9Pf(EOn1X#Cqb$yGsJ{7i?caYx=s~w!W_OPYM1$^ z#kpdzCeG?YOoVH2!ryX(2-|R>`o7R1J1eN|0+20UB&r)=ie}J!_BbNWY8)LkV{9mi z=0+C@aU#P^rYw7>p3v@p-UhwH+I;v%&2&Q@QhSIgNPq zJ7aJVk=(S}g2#RmLtZ!H*4v!8|3g&g77e3gRy_KXDB9c1_?tg#gx@RUufGwd@omIs zzU)JN3vjM0P2^IBFJ!{Wt|a!INMT?!jnS}%qM-n%+k_Wi<)qn0w6Bz)(zu?Cnh>5+ z5xFsgb|Qz-XV_;hhmIvgh>Io^vL(W6wM(cE8t|iM6L9;?Slc4O7EK{CY$Af{hQZ}R z-;@(06B?=lMl5Tv(-{k4+)F-#p9!^ms>==|;x zocqynbU$+f!*BEXjbtq(} z!FdZg=F_YWwdC_;AQ2buI{?oTB3WPh0#b(#VC-D)B}o>SG?&Ss`tCd6Y+FqPi?v3x zD?j_OP514B#a#to^EM(T4&j$I+kuf{t@XZb#}M~Unz*%$mvO(riRNRe+aR*Q$0Rb`c`aY7XmHx}fJY z1S*VB@*1A|VJ{jzHY}~vFflTXVp@Ycui~U7g5f#?ZW^zIpXT#q)&LL5f$E?I_0_Zx zM;1dwx^BAOiJLaraN<}RXNh1v{7@qv`nVbCR2l)l4O7zvOh*cM{0Aw#x+4lZ5x4Do z4Y0d_-~DP3ci-Wrdejk_R*=aS&^uf}=eZn_m?DZ<89VnRkw_b`ww=gkg#mkyCSlUe zShvQ7zkhTRV}oe~{cbE-;=;Z|8FY8&;IJF;+TIjeT1;qZcVTE;hDpg`a6=AmBJ#Hw zy$H6sA(2e8dL&GbX5eNW8$X3H%E5qX+f!#WVIFaXMN++Ue zfucK*LlqJ!3!O_KlFh>EG+He1+5RPK-c3( zMF;{D$Htn+0-Xt$AYEpY*n!C8%Vaf+{CwGoG@_>_(f>jhPJibRdY(Cri6g_vC8)zu zC$+qf(*+b26#mF;(cq&b~0mdL~$4t{6WWEF@c_*GoMCc-SsdSH)aHXra!V&SrHGNik3z@NUlN z;C}iUg4s%BKx0GZh2DPQz^1|2$Pp3Q-R3EoYA;mJ7dlGjMEaeO%j+;3NNAIQAE{gl zX=D&>u}+v*uYsdNqW)#Tw(H#pRu07)0|@6MDN1FiTvw zd88COI@UN)PuH_HtVraD18o@8tk@BlSBltuFoTMK13`}wUd@1)TVp6nM3nxDzHdok zY*K~8S42x=0hQHGw5+gWphty`2t_NAJgeJ`Rja(X_Z~Oau9jhQ5Q%iyh)5dHc`AW} zd(-&DCj+={11&73b!;VKwsM6>ED&DanZzrv=dgOE9qT&mXs!m194`=wvg7ufh@{c8 zUwLgBCyy$)<8~j$;t71~pC_?;Nd@k{fxh2r!+1E0bV5PL3O6E2B1|4LksbxPw2BAr zc47bC91bs!RHrV4x`?c!$hAAp2~XkC7syaDdUlKC(ztn38(j0 zJI>^_f?pz7410wWZJ$_$GmjlcZYl%GVSZ~eSSYYR7LzWcg^3qUBD6f?R6OD1-W8P^KTHV-n9 zfp>&{STs;yVzb(0_jVC;$yw1(ixhTnpND@2-IGn6oU1Vt>lZuABxfZW9{D5^Z@h}} z{y`CB^xaS3nSe{{ZEHo+&1XSfqk=*@hvO%8)HYf%JT`?q5v1iyeMrW0kc3}e;$L&Z9r4KjKKjFE+SS1_LmJv$YoWWI;Y|Eo+8pABEG+$!%Y)GtT8(A z!&gSIZ}&8j*CJM|rMfYvs0>TcS`mREMQ!M*!&-a|b^5n(_{1I9ec~<*4>X}~D2O3# z3l0sm;mnW|m5u?_xSz!m*LK*78lD}#7gd&VY^)2z=vxY}?}K5)JcaH0f%HoSekDA0Ef)?;aE+hEw#g8S(u9 zcI-X-NfFTAEPz@dtz>@)bF{ z>-;PaVq?dJS=GPfW)j*&ObTpfpL@OWrEDhUWZ3c1W8nByR#$b2WX=;OBXy`qRNsAwGy9=?&Bzztd;t`rtiVpO2 zMleZ>Ux(d-hEM_Fr~;c!!uVuX?D&gN_o(m#H6aU@tTZ85C*w?i5u?+(*y+~aoy4gl z5}MjH+;X24$-EK$Ln(OanyM-j@;MV~iHsyT|6e!|`-9mdCTt zMld}_3+o9JoE~818Y{YcR7{R%k>-Ms+;KU8=4K1t*qy=HXaQP5!E-yZh{uWu`c2q- zFpc(RN%*4qD`iysEy$~-@DjB_D>^zPq^X-d`mGp(R39B1taMv5x;vAIgfnPucjMUU z43;$7h}@~Dt#o4Fn>n~SRn0%sy&K&4_~{@fVsR8RKx?xd^^Ha(iD>8ZI*KVX_8hgs z=a#TO|9MPy?Z=Tbe};nT(=7|hB$}C1 zsG!!F5HC#HuBeRk9jz!F&e_48i4SYBbUG*Qw~&}I)h4IT(j1_O=ZGu;^Jtd(TUJ+qCa zM5xk+9dG0ikLsun+VSe{DI($qgu_OJ#}pV<4L59ZLZ$lJuu8?JKN-NvrDmKtoWWz? z&tS_Y8<8OmfAN1}2-VnOB0_)qoC>qagyScZSkY=mPp^U(cM{R;N>Uxl_^mJ0z$7X7 z`KxiX7+uKzmoz;01OLzdcDUnFoS6*6P_&?FiIbjBBz2$wXUGnX>dx=0!>v_+2uV4L zAD#Ir;@Vmm+Klk7v>_{-@XRaY)XF)u)MjzZMitfG0$zE;h9B>_5qjWe++Fp%XsaB8 zx#KIa+vn4l%6LEFk_78KDwc<^@(UYb_Q;5wof1JdS;HhIhDCP7V*$XSAee9oqV>`_ z^gMSOmFsF~f`9Mq)a<-}?q?^F=#E~JU}-d21lCre^~2>b4c96pJ4cBh6#h)Y;eB4h z_FW)h$PW3@q5Xmg1)8=&$;Idq7txx|1@sIf?~?uz8&zNPnJQ#U()IoC??|=J{!Y{!$}c{)9*FobY~Vvj}_pv+i~m| z&CNN$?PP~?BkG$SrSF``;aryy8<#sUIhn=bt^!uAaUoQzV4zDuJP}22ZxXB9Yp{Nu z4Y5=Xs%n74KxE4$p^E1H7}e*XoJU`sg7hj4U4blq=$^*o6%ia>lR)(6S={qj0L^tS zG;PUXkcip7!wT;FunSJ!S-3BUiHRbP?wi8t<8jn9*zudc?!sNSIf>*|6S0%A{h2Uc z-#IM;ratz)2nNRu`1pstXlb*+WX&TKG@`x9hIK34Si8!C0?CA6rGbbUmD6LzW8WRc zcb^PnU?hdM1`|f6EjZJg!>V>CEH)yNQ6j`v6_RSgo`X3Y?NVuhWI;Bq;no{m`0#xp z42-97`h^5m{Vx}G-#dmc?Q23!j|1IC6BN~ftg(PXQb*A3g4Jh%T{b{=d9lp-59G>Drpn53gitw)tqWz;CGbhWq*1 zOnM}fmHr?pd8MS_vS8w45y3=b!v~36-2kI$VFda+Kx2zdY@F*bpBg7pk|Xj|4J9!N zlih=cPd<$Bt2;3@FeDt`89d@GE(aQjU`b8~v^=}LUkvQC@xhT~ao9{$@3C-dH)EC`fqH=J8 zZ{7vz^f%MoH~$@rrORot%0GxSaJ;jC zL`p?NtpO7wN&L%qhR{&s!fkgE$+r*z2$_h`6p&97L8_Jz3F}blRnO41VIm$))jIkQ zCU9zR7HeB8(7HlKsLl+t)r8?OBm8*@MN1AZI%9ZY*(9D1CeYcMLB2!7OJgy-HW^1@ ztO)HnAo&+Y+sguu9i%y z_?Lek#&;i2p|RD2px=mr{v4e*;Le*}sH!yMUd>G1c#1}W7|3} z8j0+riN~`~+9w~VL`R1KKYn={(vXZBzUsmEA0EW5dn>T!O(){swD8Yr_gfM4nTcH4 zkvFP{5ixUEja1)W7zZCi1(CcLy8jin@9cq-$Y=xAd*6sgZ0fQ(}ea$TSgN=AgQv;TjLE;#zeLoFDlI01 zL^98HAviLQ;p$dIlM)`-=0j_Z4V`Cmh>m7)^Lio-m2MoFpD%8dJBK^T5yK;pYObR2PF{UIOhM|EV!4Y=-BGd67XAvtBi%g<-<;*KPyV;~}> z;1eJBqq)h3o^v_e|B)cP0RwtY74YKAG2FD739E|#_UB``_d{-c>>(e#M3&Zd$awrm zVIo&4OiiSzu4UYMvmMch5zoAkMth?PwUriFd=egh&`EVyM9^bHe6omhMEqbe!{yX* z?;TFz$o}+82@Llsxc!CztX3UM+d1&51e;CA@}s5JUOn`^(240|V?w)kK~NF3d=c(8KbCxKb$Q@i;}Gdy zR7Gf==B;I7a%FXiooa0Is%*cF?#W~53Y^L31m01oLbm&$WaENJ*u8Z`SQKcPC@i67 zgl@bZ!;e3KERiWzHpzAen!fybNKQK~Brc-Cnkh#rd=B~4H2h6lP&CsFap*KKJOp^X zkP=B4hsL1w57Mfs3%b<`!}29C_(L%84nww^&0_2r=$Te$6{152L_kk=tk<}R?)9NnN?2u9MpRFiXCk$A%+JQ$NvEkT>EL;u~_UuaH`4ktN%KLJ8!K(oXFr0pPI&XtL(@s z5{APHZrbLAnm1u&P{r5&QzvBN7I04g{74(RAlhtoY1&EO~eh8t-UB z(}$PBLX6+t@UIEwn)Tj6Q+&wR4@ z;DONpQYR)4jS7t#lfGX9l}9gVa5j0c^b>1;q0KAfnuG{G8XKfZ-Xn;~yg>cSXr>AN z+;%uBT199EHJ@DM33w)KgTs$>q*s{1I5Y%1ix+b-I9hna%S!Zr?{Q%wG$fO#{n!Jr z)io0dyYQ|?F|O&pvB86IhgL%NRYHqIpufHsM&7|Fn_-~Ek|7p{abf}*Nf}Yb-eDMq zMoB_fKscq)JaJ`UEtUe=6{Px#3v$YuKTWxm3G*^wl4x-v9UY*BVhmo^Brb3;Q*o=e z-A=^TGiXA?RG{_<7YkTA+KU%YTJgizH6*7J@L5er6iisY+=k)FBu0}76bd>H?TjNn zO~OBG#ZY&FNR9 za-r>i*bvS}vGslrx+W7i*;~Md4G#Rq*XnV)Gmjg$nsMDte%MVae(=o!{QW;isl8RS zE-~WHn?1M=GHln%Og08+CrYETVEx_;oUI;6fx$tLyo7*e<$U1d0US7z#ld4)Oh)pks*o@hE@IEo0@gP<;i*^f z*0+(3S;6F2CM&$$dy$PED^G! zjB5r9La^QqCK8;_&`pFVuq=e;yOyH;fex(v>;^1-WF1;QvK;kywv`;$GYXVcK?F7B zv@;^!)s=QKUiH!UnZyjda1I^6xk(VNtA+lT&tmex$i)d3fp;6)Yh76S@LE9#%ec0| zX0<|M9A>*0x^Q@(`Ct~{op5HG4~el8BF9~2+pQ?(ImPl-0FfRh6YQ*?O^(4)xm1uO zqe-UE3UDr81B=T|eg9>chV)D*Kll zIt|0APMF;e3}&+UcFu%jngMMV6C4&9nu+Fs@d@Z78Y*+B z_UlMfb7$!tD!gVGncfvuSe-Hv#RN)6M>T#qd%4LUSd%Q*2TbqMJ-Ic>b$~_ zi%Hjt&u>8E-RlZ zuE7QK>$)81863FN;;KR}F@{X|oN#*2rzVh#jYHGp@NMhB#800`aw>}EPu~rx+KSA0 zzbH2o!t;lcjUW^46S2Ge^&4UE_@PgQffskcOaiD+(eR&hG7eJt4i+W&lq^=;XpE4M z)M>G0xc3eimNo$?))vjY-<5(G3Ol58{l`X*!{YE$IZ{vyDf*e7)xjFEz$z8cKb=8V%fn6tCz-XtRb@k{!h)4+B^>RZg5PPus^w0!FLlH1bHYb; zmYpczsWn5$wdK)v)(SOgAmUU3r`wD}N8 z1>QW8fG=dgja%$^?!^S2el~%7?s1?kRKUS~5q#r&35<{B(Xm#>H~u<`zxw(hmaa75 z|NdnK{^6ge5sjMg8;>|dlzK=GJoS7E2M%R$yiCPn0mh@&ZwhdyeDi-`L(KTYFYH&v!?fLbhKd|E;0*%ZF;PzCa1 z1)MRaQ2Tp!RM{moC}ueITKKB`@YT6-`rrU9D6$AOS72Z;ho)99I=iBHZC4gsH#J~c z;7x>khp_+TT12uYY(J1eZG#1y)>u&=GC?*==o--A^BQpUtN|;VEQn|;QCs{WYU@0( z*KC++{W7j4TvdW~A#mVO4!6L*Tq{1e0imt+$V?^>?~c%fpV*AyF4oR{Jg2ldMX1hC<7IPml>*lHebCsx2UFC&L~bS ztEd){hM7o(0UJ7Z;~>_#{p5+ovIMV01RdTb;?3*47ekdJEEaZ{4o zO~Tf`3cUPA3WG$}*01*9$e|qWyvvWXr=$4hH;3T&Sa9T65vThs$PjV)^FOP?-8WU@ z|9)c@-MmF`J%yGq20$W}uY5xo^w^bHg+*qgzP>s+V~m`ZsIrxaYj$|HiCzPcxY{vict zPNlJRwHIeP73|to#Hn*#I5S|uORvf>8w#{&F`*`)6DiX$8aCkbn?ppTb-cDbisM_S zL>#aUS~aX(rzEG?2WxUFk9`NCXsngt^_WrAB~&(6pwWID_V6hj>$?@_k~X@32KU@% zCt_v56cNx{Z*@|4w&UDcmHMTMr41@nM+;h-E%1%~4`gfq70g%B`clT#!Fx`y-VRQm z<82NQ8Ci$wZH*Y&(~s;}nkH!Kco)+U&47cF=BBf#SQ|p+s+yUzi;LmcdND$z?otGc zA=%7W_Q+buoIJaXYab+h3q-6;#!`D53`96)aj>LgXLH`I##%73GjXBgYh4WI{f#1h zMD&uVXugrgmP5RcIGh0zekRx(SHjU)4Oe4398JrKkdZL4`+xC0`rT~x!X9jbMnctX3Em6(%BYW15ayA}n7sWw6F=7r{0)-hs@!2-!ZCB(f#^xMmO52C^1S zQ_=`DZV_5J4n<+1`kc+dc?S8q=+Gh9ymdlLx!|EPX24BcENW0W&Y)GM1*Q#-=YNjL zlLHuA^I>@Fy(I99s0|rK#Pc0H6of}3SkmOgeb;;GIvpJweDGR<(>(^98p&bV8Y_s` zz~iyOF6*$nS@UB?fx1*Kui#V{aQqArsNNW2ks>rp7F$&p+VXZh+Y!TE?h3s0%_$@; zL^xIjk(pA_ywZqgewx76O-@{QV*ua$hcF&`$b(ORu>uE=r>L0=Sl!{moi_(zHWqO3 za1#3trchTW;S-+-K$XoRl6$bmh?W{B_Pi0r{$m;Z!GCDPnhptH|HmkFlNmvegz9QD zrs%yCBAojVB*ndQMY^uRh^Jpp;P{E0NF?3TAi=1StRQlx7bPNo2KWOOobAcrwLLkE zjZ9%_y9FP=yBf8%5_PWTsv6++eEG~Ow9W37WZ}RFg77jTg zMpoG+cAJ?kE|KeRHV5x|v-&v3QVNN&lW+%DQ1>E%%eJFM25X;~H?T>}5om$QWFd0L zS{zOQTrB-m6=?Ysyc^cQ>~TX)vzBS*v$r2a3OVf#Qh6vn-H_XtLR-=dy($FF=|)|l zfLpUs+~G8%*6Bb|r!h=rG;p*`-h#HYmF`Q@lI=iq7|-mWA0Ig?0=05maC#wj;{T=h zyv)J55-si;or?ZyS52MLkzOp z0*}Lrnkpk2g9`E~6MDuc;qqHxmZ)B9EwI-Xar|5o$wVGiL|m`m>=7h|cYa!J8YU-m z7#cOhM5KXJ9R(^R$YvdGrxCS{cJ%e9arUT=kIGLXaL--+TxS03I9Gk*GPjP4J_BWd&v=8;Vs(Z0-%S6)bA zACZlbt|Su47;e6+N+h3dYgG_S0P9!V5%9@Q+EWk3ZH%CkLLM4~!A`_XFX1_|H# z?ihkZHZvI&=gy{)&MN31EnsPz3-{jT#Ih9@{Nwkh5e*yhsR!J+^X?YhxXFVR?ZD1A z6YvELm?n~UBlav}#wfN`fqw3D&O$veVtLx~jx3wzo*hY!4FZHg7!=6Q{-B zyo+$5qmutJSbOCna&=Zb$I0=kIFU2K;BvqqQ9tLnR5MZJb>7*>=DUl5-<360ZZ}fi z9E|NPf@JCSLFh|cfR-j$<7r4FQB@Of?&nMB8V*>@I|!NVwbswX!kPt$rLfj57aK+) z8y36%ev$ln4hO#zwIjD_Y~%=$lmH!Gx+m8Ke`jI9Q=3ljuG2hLBqCIJW;b%L9m3Ft z4e+g9hHN~GWK@E~s3Ve9q3AjEb*tz;Yamh~BTf}~^G!}f(>gjkG~h=;YL^vRd^H(@Qn2GR z5rK|XF6=p;K~>0&>Yy1%&m_V7lIm(?>^m02#B>gK+)xRb`UnU94Ab9JQDAJufDc{o z!P*r*RC*;GJTQ*vxPs3;Qi*lfdEoRL5gSvne}4f-juhZ`8IVq!L?rfq_mz-HgY(q0 zVbnCbv2vx8=3H8|wmYzH8I@&{X#+`Ropna(wYu8xN z*k;D~q>58V3&<7>n3^VH6tD<$<<>1;bWf-_b6myF7t+Y+c|<>!g||w^bt7(s=w5@v zDqebSnux6xFT56o3Nvom>O-)`j^3#twl(cQVq^+kWA`F$$_pPg4iRA@vBmn%N{<1b zd&n(_<|}(LSk^2PQBNT$U5_e#H*AURG&VjlSA}gE??b$=2^NDLIBy`m z0~3oYDj$(HlLdyNip+R7Y<417N*)FxCwgTiG$L-=iB1~mys2eIvhXfKc3$l2g$N>4 z6C{#V3hQzu5L!68S7PkA(86(|?D>)*Zck2y#3VO4b%yGvgE-ebLO;9Fxqnv{r@{)KJ}0jLm5#s{j)X6b_tPMi~rFn_^IMv?#EZw(-vcyjWm| zY}O+2F2eBOeqr)jOqe7K(&28%&I%D6RK$+5Q(01Z=s$T5`p(@%lxUD$Mq~R<(4vP( ziPc>%2qOD-NnAgi9J-hlK|#fm{q0DdO0B-Wkd69t^@&cH0o)TcBg zlmaxTf_UDD*Z0KGx>Ux$`AQXDcxDzB^BK-COSb_S#zuk{rJJRsF zZ1~uHM#zeSum9}?e)Q8ZobJgZ=r*IK!hl%XfR^P>+;+QPIP}{cGP-;7c;od9t$bV} z`u)w@%=qxV9+>nZ!joAz9VQ$DaNu|ncWhDcJ>z8 z=Bp*M`jn2`7*&KNVFqq6jvy`0dEG1($n5cLQr1jG{PM?k5-HiC?F}Q+D zOLg)Nb~t;mS&*kR`WK31h4CjZK`~g6x@$G`j7mJyf#`G^E~f>ZJq5T$f@2wdy+#qB z6Gry;Nn<)u5E_0jkr{)%hzfTAd4~dbs|H2ZU{Dogrmfg_D1|zg6>HYpP&8?XPtkL! z%HP->hT89h?V|?#Y~=*L>E4Fr!2lkAb{J!$Ktrtw6;5`H*QxnU=sc0cz<3sC&#^X# zwK6geozl?SWI!foMpM0nFMQli1WUuN{aGwu>L5~Q!pU=axa|_Y_jnW``p)=323==V ztm*JjnQ}>+GR@`+<0J{&xvHy4sg@Oq`er6y1ek*Rcp&o~iC9rRQ8XK3piC|d~ zk7x1aO|{5>Dvxh(7{(uUuEYa}sUOKk^wCgGPKDu@sO(K%IEXlQ(>o3h7BNO7aLp14 zCQ}YN)rZF+;m>}s3ij9(KD^~Gu(_T{@Yx&i^SwoETxFtqj6uMvU65 zp~{S9?LcpK8~nLlsIVS{9{h;t6J>m`@qy4_G1%!{c3Gf_#L^c5b;x`s3mbLds{u}h za5-?GQ4Sbb#-fhHuHrA0?*`h zWFiBwd+O*Joa?h#3h+4=22P#3N8YcSw%(WN71wLbK82c`nZEhl2=OB3l z(8Ez8SSJCqxunJ7dP^jsCsHu9FM;6`AEtp%&&^z+DQ<=+rY6q{a~|*f)6^_|aQSh1 zMG#u7BoWXRLQ57IIsnP;DYf}LK(7sj1g^^3tCL+wo*_1K|7If7IuzXxPqh)7Zx7H< z37$F$MODV=0I<2mi|H`6n>CFXy<5g8!4)uL%jRa>eQ!JLD^z%BaVL}DpNebf=`*6r zrNEk3F**W__iGqAm&OxMP9Yj0K_B#D`C2Osx8|`zc4B?TfrGD);+2=uaLBa4J)1!w z;G{00qIYN<|MqMDKYQl^7{^(r|7Uh)d*9WncUh8q?k4|U_0YX9=>2Vx8cH-V`$*NZG+xyPU&j0;JYwS33;vzYcJwIooo!Q+f-<0o} z?|Z-J^}+6TWB~J4!QR7JI!&@9YT|hGr6@l3aVKuS$w`%sgk&s^!M=nlWWpN!+jV>s3!Vz@`dV^0oa zs5gbB3-ze2(82BH5l`xIxFLdNvl3V`m&dP;{4;jk4+ zxM~HD8?LoLgn*ZJNf-{wFp!W_GI0{I5$u<~26R4*!~k6v8dIJr(}I;6j-&B`y>!E5 zCZuCmFrzV85=z(CsJ>bgP0zMA4Daoy`|cTT;50i+kA_Sb&U)`6&8FcJMs2-v6saCo z@qtjYL_50J55G<;3}R!8h17Bu+}CXMvDi*p53rI z%T#}@3vE?QOFG2kGNdP7M7p#JeFX)`3`OrAc1Daf~9s6og7An(&{Md8MTF2RFaQaN8I{#~TuTBm9Fiesb*QVcqP-=IXePfp^5hQ#qAMzTvn`DYQ^SP zQy2^@m{v<=;uO$$EQ!aTO+l8K!Dkl3fjFv*%V9R=!N7;qG*5>cB&?fdqw`(H&*$~w zi6sGi;dm)-+V4ifB|)LWe&yw1B$6VmZaaK=B&Hl3JoKPpw{fWP=}?eoM=&A59{`MY z68DOTX=MhqwfS*uWF;DhR-)Ya4BoZo0TQ!mD#JA}+N?O%2 zJ%j00bi93wp@iB{n0^pa>3hMyLCmqn=eb3YZIo1%$J0jtw`%nm0{Nal>pF;If0GiU2RYqFM&1aZpU?0GvQ$ zzl5UTs_5ax=OR^^bC+i=I9A$!g9Th$KX9-Ciq{3sLg)Lbt$DN`%w0W86@I$C8KwhW zv=`E-xKijET7Ly_)hciWE-0zw$>pCF9Bn&G#(Y@qN*(WH%qL-Kcrq}Om}ae(i9iSJ zMRVvnCsBGH$6XyYNJS)adIXLQL1<`$Zdo;sb}JYdFd{?uyIs3UOzUKLJO(hY;%Gv| zG7=U}Zx#cQAje165!-Ic`!0D3^ zh!3ME--N~U%xYKRa708ya|ppu3R`~?!U&1ezb*^HG~0-pW&srz0g}ZA#gu_tFJToE zQX{}0eiy`o`8E_*i12yrSh84$z5Amm@ETD+(|}Ts1>3h}uxoz|u|yg*RZbFybXsi- z!&hj6-JFDv?u!LQX0rMF9g5&5YOyNJ8s?XI!hsNpV|Hn9bgp-GiS!`NLrlid++l}Y^nMa)3O;((FEG1g1lw9yW8kQagWCg0 zNfH*!a$w$E6ADR~l~-Djkx0nur=dFiGYqHekhRytq&=5US*DD@iuQ+b_-8xS)~hp( zBvha1>3&_bqzomiCq8A)z@9GjZs}Bq437zIjzR*9Qn!u9O0u ztIuo!1_uCo>Q==Z8{L;kfMXdJX3vC@3n}EBB)0rfsx@~48(#uOg5Z`cpvQW;E>0Z; zLfe{EBna*T@OxXpH5^ktt`x5q+|Abl^J;1NF(6Lcz_jDp?wh(FtQD?{Dz-43#dAhm z;Vz_X-Uzl_I_|8+GT6IU^)547eQNOgxv!I>^CpXAR)9Ddgr&b1a=iz=aVw;Zfr?Oq z-I+y@iub^gFv^Nd$anK_=Lv|!1hsXkCf|(RThn;#*$`&e>R{v*Jo97{Gs}%|+Oybq zFpO9{gH%GnQ;!ZJl}zLMw|U?#u)uET(b$;9BaaRsOaiF9$_|fN!OS)*^8F6%yC#6G z))XpVv7o0@fu2O(^{cFE!s4wv($Fb-tX^!umfbzS%e%mR25n2mG)(bqI+bAf3XjYWkCAqEA5Y zu#9j0pbsP6ZMbr6F{)>Hu%~&5gqsMbNr%3X6bg%-^e`=;u7c$|6tHtoCzXi-a^5ZI zNG!ub=a1n{ZbaRjcc3J%7{7d^6S07dNRV}z@^l{P(Ak;5%%!v7)g6Ek+JUh59<`TT zV`}m`fyJJv4*hl~coGfPe0oHfB(y$$1g(!Ws3v=7Buh71lK~|gs!+Ui;=2e3_IIP_ z1$yFSMlfT+?!zRoc(Z_6?_C0uX47!Vkr--**`81JZLUjfqEP5O(wo#E>tN3wSlyNC zG_z4qPqcGJ0ZY!zM#d$Aov;_o7;SB$>z=c@!EN0KWzTMKtZVR8(ZEI!_Vv?u>3f9* z^wdson>)chO~R_BnS`JW-G;T`YRgC{F~NBnv@Dhon?KY7rMn-xm8*ao*8#;;sYN2A zg6te)%v4(jA~J9U_PiQZG>-c~fYV^h*IPX5agPifg|l!b6(3vY*~9L&EIw1A{Y~?F z=o*hB+1(EN>RC8+Opj-t4M3Mlp>CQ1F=jeDq=VOE$MjkY8jg{0Y7WEawxG;+b&Oc^XTSa2Ixh=yz9=qQV!<`q54%SkX8BpmOEA{-S|(bgYGpu1CmPE2Fh zei1d5CX|+B(cdd!=OGcY%+We!tX^YBiPwM^Hv3_9m|!-fVI+Zl_?U#FEh$W&Va3j4 zDSA(3B5`;fG?Pjpl}_Q&3zKK%zbbaET`qp#j`v6(Rd^TkI{r0PbN!MbUE=GCmGLa=bWgqWu7-MAG)W(NjbGf`~Op`mFI0l$I7ik<{R1{L`Zboa?r2`4e+ zk7IfrV5TyD>e&SL98F-u3M+27&Wqus1f7*`hWa$x+9D_~HXxlcVkju$s&!rnIz6`S zOpwT--H;XR+8xBAMPB&mdv046dt0LD4(MR7)?uN;hxz?H9$q$#+8Ps9bUN|t7lPQk zH;UyeEqLn55xlY^iM312=;>xQ4L!K$T`ttk$iu$<5jD@@^yyiY7V9y+%7)r2>C~o>B!%utmakcbL%jtrQL}i=!lC#@i10 z@cBq3ob4w3?{7NLO#8nI}B8^>Bl z=sM()p}HnZ+w8QZ(0QER)AN-KD#JVhRfTCZvf&?AA<2?!-zG~Fj9|CWu2L|t)+)1Y~)p(7-Vm>U-fA}G! z`Vu5mBsBbb58Y&SYQXpz1Gz5(4S(E&!$02zQ=WxAc%Kn9pxma%;xDd+kp;+3Fhhs> z(D@V-SZ5Y}EGVCSKI;REVbZz?FDa74O``$lB(x^5vE(H5RLsN?8yy$w+n+NcHPd;Y zRJ#z*<;9l6TCSKdwEJd3;YbWcNF=_xAK1EG6;UwLH8e)6oWi;a>2oehHD5e7D#Tcd zj|BaG`2uh?~A87csPV4{Z7|#3=)@z7y4{)>O@o) z8F8Rt1eq8KTGFf0L>w-+5uQR0gMktHoe5<=0nJ@;6qo5y&U}^DyHQnRLH_^=PuYNE zFowZy5$RzIL|Mj+`LwKr0gbH^9bO3w=Gc&DaUpI}Fs;Up(ma{YDG^?}*G{jrq9-wg z^0*bgpb7WR9)RhX3En;(8oN?z1|ywb!mJrKG_|mV%Q0MkofADn5{i6!+D9H9w*ejf zY1CHm*tso*J^NTUs*F%5im;zV3TMRpd1jcHZ&rq*_coN5>Pe;~uxh>yN19|DY+@D< zI#_iHJoE(ZOID9%3rr}^=h4xX#Yi}X0TPe>B!((043HHWMtZ2tcN$UYA#u1efK&;O zue~r0mS6FRH1KHGWniCXMP9xYc1s3sABns@8HjNaMa3L^Bxdsq>A~5kpsy zm>6Cou#S?DIrzivFnUdDdKVV&)cI@++Ma2EtIn%hC0uMeUuwoPcmEMax8;nZ2N^6D zMfJd1Q;9hrT6R$c;mDDGbUfX3<}N})p{Eiiux7qzA+adEk{@;Es2W z2COr|bV`6alUeL6XKvZ-GUVy{;td88TT}_1*mse{o5uyzR)e$9?^&=stA$T58@3-} z8hX7G(SZg?>FB82I_;a9EASj%EGVBjw(G6l(blU}h?akDU5w`q?FVaBG&zb853o^~ zNw7rOY~EPgdg_Y~B~#mW$^3zL3}h_isAt;Le31RCP8;Y znuLphnbUz-Qm>knx*V)AF$29W3x~&sa8f~UUjkGTm|kl@n1pUgsR_|Q1TKS)gq9r@ zRSpD$83g=klvh|u968Xiw+o@5Ld&%w-L6QB{{gR6d~S_!c9G;|kG zN5`@5uXtAkEq^(P!GpbURC{16aL~PF;wzM<#}4ATcR!@Aiv^0aZ8{T7P)RWni~o5Y z3Ko>+=1wgBrXjRH+W3YoSj-2D#k0+L_d-~hOP|IiK|0zGvo#O0n&a*bnpMXey6jj$ zIedRgc?_SUE(i*s0Ev=`D&B}Y@zuu*;N0fmVtg6*A-U4uM_ z7=3;uDWNwl!bKv1wG{l_t$`Up_6|bn9t5Z8z}e`0XI^oqU$-=^XWe>(jcQ9y;f(8H zFu8Ku<~ygC$(FAsa!yD3Xc=^0;tjO?i`9a~g6nPS2uyZNd^PLT{W|OYu=(h|=B4A8 zM?%96xv&_zt|mCC0`DuVhQi7&V?r#Qf`bH>)4_q0Qy2(kkz}z?JpojaP-;FTl9tWV zu}qNgu|iMV<@Fd4j7g}UA;4i%uzybkzEU^SAV|hIILouJ84Y;m$tW5-Vpz4(jd=?_ z*s)K7O>aV}*M`45-iMJP6MTgk5*8$KvKBmQ_9L-{!*UXRHS-PFwLJ#6M@J&SfK4yS zYA?b{mZ~Pf;l}F?B!p~eI2^~0ol&|+XE3eUfPIHq;JyW`R+wO>Tj!nwQ9Soz7)|XG z>S`=>+9?P{5@>H{c@fQsM|9Y}FNv;!1Xj(}A)NqrAEsqh30Sk-gWU&`s&Q#qg_-Wf zGL9aNK~C!+$1?cq)X)|6rG z6{Xm{r2`4%p@c*(U0!g~a!pPhid}jXxQwuwqL8=(_$_y1NLq(rFoe0)+pwzUIZSu# z#E9$TIL3bpW%;zNjx*OU8k3v93#?85@E8X7_Q1@#;r@QdR3Fm7)5*(3&d7m&G~T}t zU0Yfq*bK1v=wh~vzdLz+Fp4c-`WtqA<0ZO>(aqRFPs*ok(^zCPEXNavHUE9XMeB+S zwhW{7uSZl7s`d|zg-k-#z+?J7^C#7y0gVeopvpYyKTbl$OCl?MQ64y0h0YtSNU|ix zi4izU7OJf#^ug%o=K$+QXuflYxyG~ohz=ZuEw5JHuWTmyx@{cm zm6dZZNrafm(EhXG{T4)YdX#lFAdy!FU8xuK6*8JTQs@f{sIKQRr(Ta~MJ`A>(q6m_ zpVf(DhXZhsu#iYS^$o|dYN?k50gs9@9eh-&my{V{DR$y_f9%2R8G1}#Y=Nv3Aq<$X z6|Kp z%1U+UC-EU=QdqIVh0g2f-U1`)XSxv%XVKV3qB0|3 z@iGS%FXq(l#bYfg=oAU=LL(N=H{mn2C0NAeBmGPogD=uLki!1?BM2@N!8;vDAI-pj zFped2^N^tJU}A`M<$37%=DXB9pqFRZCx33btzap-GH=Ag3DvUV3_tPF$8;l0W$6zpxD=A zU(YnWICu+I-Et3R*3x=Sbgwy+4Uop9M-^Dz+uE?{(@&gP0MN}-O?6?_(~}y9=eNbb8{!2VA}|qA3UUXr=Ce*F=40^c+|apE^I|wTkoX;8@VR4t{^c@LdB?7 znrA!-#tD{QhOIX;*q{mtJsT}|0SSlI?k!tB68=ue$%tw-p}6RdUS9(glQ0T~fx$uO z%|RC& ziN!4S>{pA+RWe_VTRLAs#O^n&?AeIExSw@-+US9YDoED-Hi4)Qezcy*;8ArnrY*l# zwaqx&{a|1F`&ShX8A(BwYF>SDA>zl{P_0jZpFaaO5;`??b~Lwy(LbCW>XFnycmI$M$6rb#ybD;UGvQ7lA9X`cba5H%tQ=d{HU`KlpDALjCE1_d3gQ!)+BfI_RPfC#NX&h>gV_mHa3ybVj{v1eB!dGO5UN0jV zpzDXubxBS^Fc=UbQ5g3R!dL932LJ&B17VD`#js>)Ataj#f|$k5SBGF$cuXq>3S3FF z_j9;^?+R>KSA}ULj@h}biL0p%|1Y2a@;9D&*PoI0C!x2o=JnhsW04bDEar%HBT97Q z4esd1@h2Ne80|vej!uL-0*H2n5I*jQ-o>6mIHZFqh{+7yfFz{DDfLE@rGLXm`q90q z4ZS;#WA}G9;gzpGkKWD4p=9WZ$);CNus7Tu27O0nqd7zwH$M2TixS*^0=BPBe>?!e zOclzR?yIb~f-aKUJLgP_5t$m}$7m~78%%UlXJTbSp$eUKN5w~4sSXlH9oLy_`~``+ z(V%(ODi){xFx#k>p=%VG6jd}{U63*D&fF(9>rb`OV+zF6cG>(OEA0Aa69x! zBsiF|9D2=B94(c=J)T9dk&YV)d?qfsd!pETNJL$o2L_9bO0Kuy!be zMLv$aXM$t4aiq_gN4wOYPv>=7!GZWZd7{Knz zAlgbvIAwGgIhIC#l%79&Q}9+fVK=8yHKT;qt3%JA4BnIvmoE?bJ`VAO0WwEr6*@%c zyc>D>`55UKg271J&zle#iIHf|;N^BNW>m|#?Mh4T+|Zcn@YfnweWUS@`_$Gec0@{2!6Kx2Z@2@vCcRs2k(_mbNEL?SLm0?Xl>p6h12&{i|L zZeojvq+TXkc+=XfqtK*vS2Yy$xxTt zx@5HFseJVD|T6WP1y?#mi9a zl931}ajYwg>MD9*jp=PJN67BM9ay#lCZdw7@+%O z{WL4qFSlWsr5=hqu=B+fLct{F&i7%{&KPVq5?jkHNSiX~9TefB2i0lSUJ?@-q|*r~ zT>=j0XYi^N#tnmZ)K%yamjn_ov<-0`Ob#1-ZUF^(GHNP}(Cc_K923#ilg0e$24)I^ z%{$`Ii6W%1OxvEs%;`LA7QGtGzG{UVUYCLNxdEY=gyA6>6%_*XW**BIT41H^8R|)s zfKkxcp+i$s3MLlVF4KCN(lF}usIFno@Cv&6IUH|JqoIL>i&aO$jKlO<4j2s*2D+07 z)AA3s1@Xe}B!V6EaPW+XcN={uI&4PbaRt+3R@8^AST4FystO-&BOQFqr|to_c7qrbn1_!|b%_UKV6x6$U@u~6qyc~;*t6YiQkb)Lqh18eEyru1CiJ0U57Pb41#bz<##adn= z7?3@343<<9ORWYh$tJO$mvEaID6p6zCDTwG4sbVJ3&lzgTg)t!1*I!^S|)HLc;mqim`U`UUyIV4!CJ5An1_0hjt|3GvViVsY(LKL z-lN{~)N)d22|CD<42#`}VVx7U1G`|dIxuAMQKdYLY7!hzJrlz-k3=zRh6%MKwg&wv z)YsEFVdc=(mB!&?DJ)thz>p7ghZ4|7O<1(lh01y};%OZc841nDB&3ous_TvLmSr(( zjsP8riboz#!kZ^z`3gIHg$|^XIyh`*SR4YpiVnN#qtH7Pyg!?dt$QO_x7>^lmKlkb z_nuqr7zqgQhedq!JwB9|JMo)8jF9k1AwepT#USZ98KrJ3o_J~ml9Iu!S~oWD0|_W6 zb{vc#97sdpBphwZ!bjUtSz<(aAxofc#Djkgz(hjC;c*}sNg)$TW6P!>YHAIbJIjiE zj{p-DV>}`vIwC_Sk${ZSdONcasBU7tEiS6F9y90a;V3hb5CmS` z6hMKFL+`5+4m^^=TAu^?BPPW6v%Iq+0^71!(wB#;6Hb&;y|sl8W8;Dl{Io1?uYkp+ zKA0^cnhrK&?@OB@8A{BvebD#`<|v%qXpV3HN21v6&WWMML!asNwJymJGG z*)z2;l1mBxzyJ5U-MS zvHQz8!G4>e3lfGb?%en>dLlOFKDl@dAM8ih#%8q}@Wj5oj?p_PDmK)@JF_4+Q{z&C z2?uo)8#5do_Wb|)drGFB|Ipt#tM zh4V=y2leP|w!q_2Fcgi#!0XX?OvcNb<0vZ8)Akz?O~)XndFNOW4%ma{I}j-KObq!Jmr7O6b;v950RV3Ne% zCW|U)?7A!xSp!;I{a9UXM`@iEtsN=6LLx6CNm#woh!|_QrPH`QAmZi2GHPe(;jWIU?P&_aTi`Is}W7UkA3+FDbH zr*ueDIaU-2uvkdYDgv7K9Y<^=h)1^Cv2>OW*R30U0B7CB<9Z9xc*{_O+u(KPnj?id}6cb11D10Uy^~59f?@z)tEe~(s{BbzT-Wnq|)`Hdk)G>9N-s~n` zd2K!NW@#x8E=8CtH#5m(37^%=h20^?jnO2=Y^0jO#%u|gFk!Xu_}zioxy!@}TQ-Zu zg3}5kJWw8c7TCKNEZZz6u#tkJsa;s()QPqsE|;GW(Kt|BMRye1u2cN8R1~VDO-@9H z8X>2n@K!8?ls^B!_A|pSM9{h%RB^SaU52bxYusA0XgMqmPkgvV9XaYOnmrD2aJgcj z|Ed2-o3a)kvxWY;J+L<(!J)%3L=1UwTSO!k1FFi6cyV(AeFI6D1PKp5n!@r$cHI89 zGAv(g$Iy_1&fX*hI$o)`0$E{Qg;|;(gB-HML&8NNVdE}Jp}UvI%sLlU_#8?r%p_t= z=jAR#1kSu^3Hs8^vLjs zSYAE>zyC`V?QNo(Cdg)&Fk_aLN=J`9JL1@~GmX`>ovdr}AeGJ0#w50F595WGBiOMw zhMjv;IMkA%(^iC)_H|%Tz_R%~b{@{)nO6pJ^(-ffORf0XBO}}UsoN+ zP^pBfRu`-?kE6#&P+-@gzQP0#iK;+MgwD*tC?~;Nt*FV*VsCdD4zwwF$9fSSmO%Z) z`Ji!0@EW60S9PB1*QC%rbL0>`1xC`)>FH>nsdeUJL))Q9bi+tWu-CZow&y(!H>2wM>6*a0gb~ax)fwGn@s3|B7VB1HwhWOTh{+I~g%g;>nAWA! zlaR7lAvU+c@WLJ_EsZ3$=yCq^bUCN5-$){+r=K-h1I)!;d~JX^ix-ngz=eAE!Cf+6 z6^s*Q&dQd{MieqL)pT@_gheG-a%@`J=oc;kX3M~iZEUD%)HR;OrE8PP5qOK3 z&yo>NuL7Gl1I3~PyN#1nfmJVH_B0N^y*~~+iLUEzcajJp5m0Z0#l^$xHKM;;!Lj`^ zi7o}17zwqEfUP$~P!}g5_7e%VX*x_Rv!bmvidBm|XzAmSPa=rrA(Tnb)>j)*UT()- zw^^`wz8?L4dPv|>INl?nke2`J2ZoSW;KavvKSoBA`;1cH>AN!JQla+V@b$_ADg?eHygv-crR*6VD|RLk?*yj zyDtuxO+aCZ7r_C#zQiFoYi8qxqh>6r?8n?$1+Y4_OdppTuMt=b!6=~O%4w*%eHNlU zA%xljkmHhi2T+60C*tP*X0UbAOC-U^jO^;)IS)7g>D{pSY`HJIH4GnOU4$*UsWZa{ z1F$Z_yg5a=IU1K5tm}x4Tx0p^bQhF_`4pHf11*=uUNNCCp@H;tePtoRVD>_IelO5) z6r79173=^&0}>?O;ie`9gg^9{4=M>mf(b1|rrX5DC^p1o>IpR;HV zq*PR0--JNRWo2)&<{>`Prlu2OqdhOU+*m6dmA`7sU@((#sfLtE(CWr-KiHzExKt|~ z7N5%OZzNv04jW%iKu`Plfp^&8r=nyw2uTuMJNHLO04T7MIP-d0 zYmy#Dr(Vs~#|BrkmYMx5`1>I9l}cmbN(ZQDRR1cEPr&d%5G|r;sJ1jibt)|x+7xo+gnp#s>xzGqjNaI+$ zf)Bj20C6RQEj!}4@fr_2ZVtQmajJ`aV^f60nUTs(hhRjHS+jJgF3zH}Pfw?Q7J>dC zf&mg_{xnuC^?)`9!^3Hi4#tkIBux8wxLSGiy(5hUaVx47GYX51;Ax)+hm#OVASMzD z3Mzf5$dBQ%ogR8N$>789D20pJ*K1sIyiQ=9!0fi5?#{U=+fbt#;e}fLkiv99NHRUS zaq4TrQExa3Dwg!)Kob<4~{94{#d!OfO@N|LKVN~4{P0{{YV7VQHrkOxwHxYc8BlD>2JcvxS4;y!7ad=ke zxU30JKM_J-IEAueGg47Hf9Sj$8Wd4hkVS?ze9>GdqW%Q>`Z9QNyM!UX4lV5xy6HP^ zCy$%1wZWBdKq`8)$HBSq(v zjHUDSXzt8n$vis>^D;Qv5JE|z6UA059HIecQd?#NhYG6^&5IK#Qw&&;p>xpA4k<^M zj|ow#KSQw;i7E@)qczxeSclKP!vHTEb*phnaz=r50$Z^Y({7%n`Z(DtoQU;C)DeJU zge8roQ%5-S2)YY_B}rx@51B)|wcLp%U%3(+{`WTcW>210ptBNKL?Rr=;xDX))kjyG z#w87>Hz5=0h2HGGAe(^E?x>XNd!^bku$+e%>KAnuu+|^Gv=|AG1gXRU>(VQbts;>% ztrDm%16M^ade7C+bZrfE%#?IyJ&CK?Krsn1X78Xfx}B;mL&tVCsNI%VJsPvb0*xjT zD)VeErYsTYpmI}S_LP%&WXYNJ=kYSAf-TgykM@J5<|!KYI5>v7IQk#EuS${vy}-N% zGZ?V@VCwHhL9iDObrd2ptdJGcAkRJ5brTw5H*(0Vcf; zc|HSNP7Wrk1xH#$x;G}UZcQ}~wnbqz#Iba)4b`OvH5F!KixE#f;YT={#*J4yVY4c5 zIs`oaYy$qEh}pBOC~zBa>}UiJJsQJ+UxbCkTzQEBLxBuxEA=QUqz7@Gf|7g_yjB@I zb_Z}pr31RO1zYKNYy_j*L*h^`!9~lr+6$01`5?T@Dj`J!rgT&;zUb+;rARDG`GQdeQNG6S|&jL7;J1ee_~+Q961I zV8QXMON^zhdEEmZyMc@`kKIt1pkjaJI6XOA4A5D4l&r4AtPdO4E!yorz z>ldG5y$7TFHWpMiiNQGDzU|{ET~(c%sc|V2>pu*uqZlkt;rYerjJ94y298jD%fnGP zb7H&IUV{Rij>H}P3v};P%|P`eRA>W6uU+Pf$xe1bU`;Kf?-|V2K|Hlvl)gv%kQ{DN zlQHL2uT#6}R71{-!f5N6m<+3aRm^uwO%{J{w>uSf3|Q+GODydwW$B4nS)BWF&IWcn zX5xyqV#Vp6nn=j#Pf%HpbfISJR{W-~8tujkbR3f)lXfV|OVf5`asAZ}w9~WR<|hLr zQmnL{96BNytX^7#%4!}m31_=SheL<`80?cVqt1&1tqGKs0CQ)W>E5m2iAPd6uw6!R zwSp_JqVJncB%n;#)0o8LcMRdiCM)cJQ1Hk=8p{?Luwa%ATV4#Ii>9YlS+QiU2RjeO zNc<&9Bw6tIQ$Z4V3Oc*9B;X`0UushYTXCUL?RtCgNfGtsJi0q1+;EEtS1fX2_qG(e zNZ{>hNx^ECF|*2msyaH4A~6I;NXV6&@bD8+bu?ZrJ!>Z;GM?QUMnxHip@H)+`xTE02HpfD;crIE?<06c)|VAvr?ZG{`~kG-1^$Cn8B1ukMi0c(jd9*&(dG z_Ifn$-GY%oHC8RMp^)~WZ&=0$@9|=9gNOpj2!Htyp5Nfdh*<}VXn;*Ip+IlN3cCmM zB`=hAEApHc#Hqeym#xoB2a8UbjHsZ1#Hh=~tH)U5(&n56)~PVDvbf6BAoX*F#ngrR4#Mgxhm!;rGi4nY53(^(OO@^!B8jX_ zO2*)jgae&vIIJP8-SaAXh9&$cvzjVw3%pJV8@EMq-IWfth2_a7deM3$jY78x1CbyW zFU!Z`rFImTn91^QV4zTt;kI;lzD^>xPXU;+M|%A(B?m)u)WMPURuUbkV8SS4%71;Na>UK!ENoB?XARn`>CD^nc&o!;G%l1u*3+H=VG4Y z8k3m|5?H4r#xsZyMG@%?sx4aSR2u!Sb|BUf0rQD6^Y4UKb!#bTvHJ?e2w*BPPPnqabnh65@glZTnkLzWF7Tl(^A5Z4rLCF@>y2#OxVfY<$*_?XQf$ zV7K9uAN63(94C(M3gW+h(u3+64?gv27xIfuIB-Nkdv_QMr#Z1_Zwjeo7wKSqXu zqsL+}I;^;MwG|sT55jBHp?0Pdf`i9EdlbPij}>zZA?ZUf83nw$pCf^jMs;Bp3+tVj zv&4z!E*by+UtPHRIwNXI3^>+Eb%NUnn>~dE)gBBDr|{5|an#nBFyfEkAPGr5$1EL8 zuv^nObV!CL&w`Zy3C5dT-yx8Iv@Vjb_E;-CaTLu zJso-0YonoY3garUG&D5Em-yf@Sgbxs5(%t}ajDWec!LE}Itq|LVO?t%3TqKd3><~o zQ8F&RQD*~-S!1nb{?1on&!4Uukjk0!w75{knnN`g&63b%-KNY2V!Q<}>rH0egK-i$ zvXn*EW`;kMhWq|MA|;#Au=*Md#zYtraSRS|c>0wLoJhk#=SGZ#d_Gn5ET5p;Za}`r zj?SJC>|O(686(1xD2XDP*9Nq-BvIf|(BH2Emr0>F5Q9R$x%yfsd_Eq0NCB(5gVkZg3!8(qOdFI0kBzTJ;C0(kSt;UpPZ9$I z0y1I-@4Y)8?JXhP_mH2;As>3PgdGQ>R1Ri*;=|>5_-Q{9(X864#_S)QE(t~w(mM~u zN$_QH?FKtsdIv@_LHzQaJ@~oldTd|=JGAgAjW^&;gWNSVG~PON1{1_EbBXtic5h8| zD;7&#rPEWz7>3zhk(+s8uFWGmIX)t3=)hNkn@m! z$F`Y?w0Ph+n6o}>;Tt#04ARXkGL1d!rqkI`fkomlWi-R`*i$e>=`t&vc^ph6qzgn5dwY!F%m#QYBFu^bOBRBJnh|EB1r04CtTq{iB;;HU z3x4wJVMGEV-uJFj)t~CYzecd*)e!RSdiZGDLJ=8%erS*cqZ!pTJnnyF2&F{^%%1K* zL75f(BOL4|J?d(#cz){${Jj$Ge+-;pz~Ti)#G*Rfd1D?L+mbkRB!xxu@=#IABTL6r zR0MdOChR&GgF_E|@O`D2HOq)yJELmKp}BLdC@j>&XkepdO_*+!(N^w9W^O*dk~f>K zlL@{SHKsh8z|zo|#IO-;srU%=0y800t6_EigusM}fhzC#5DczTRY;5%0tu2-xEDr~ zi^SH2SSGv~%u>NXf+~)fzZK5Hx$3+N4X$Uu7xbgYAQ|d}*;PL7-JMkYs#|^_oJByk zK)-1#Oh=k9a>vbRk8;?zxd(@L%b35=ik_h;GJ`;SQwUkA_*brU!RR!ivcdt20eE@) z2#ksjmH7r(?HouSAT4HLvzS$fcV=bga2dea<5;-Jiyy#0U4_nSuwlbfIJdy%*TqQ>6u;cz>_aZ`28PA^p&LX?mJ7slfd)%ba?#9 zC`@`Dw_fK!)&jJ)DyXZp!XJ^4l5{97Gpd2=jmI(=4)Rzs+l53r1ecA23=8V(d6bYy z+VpY&H(r&8!eTRRi%i#(OzU-H|B(m=22!~D4j<;NwIUXm)zmvfL3+rLMBH+%8wEuc z5}zba2a?!!V+MsTFK#iGs^VQk35j77E6Y0qB0tf zRz)VNWO+2OJ((ckV6ua6X@hOk795$i4ny92I7d?S%_uf)OCaj!uxhah4?Uc~qG@J) z^~+_DcmeS!kBaGR)SV2L!$8d)ZM_L02_;si$@F1Q^!|D)`=heaD$ii&bqoaq` znUQGU^q4+fkJ&XI7)1p&#TNKV4QT1gqL#|EZFK@lAc1L}2Gq_op^?Ph?xRV}pJu~V zB%IPDK%;IGO1wrKI@*qvGYe2izds&GVp_2tS1xkG;pQ;C#)4ftBG|Pn4zEW+EU94k zz9HPa&W<%Jz3BAEv2>{oL%kx}+7fv8U4;n5GuV41jnzx+RNQ(b=r_S&hQz2qqEe4A z2_ui&fH`v&x=whA2?4FgMZCN(f#6d5^*d| zjd}P*$zD&Z{wg2FHal9 zFV_X}9Dfb!?V9Vl#@V3>EDen*0BiM01Uq1~<#HX4XDb#H4kUi0R2VvoTQvurC|I)O zRJcdAei;9lGENmhEEwJFsZgyZ#?=s%mc!z%*tRpP2IjG4ahG54I+(!9WU@%a6r}Wa z7`JVQB@;r|+N%*Biorwz?nq-obsk^7#D$sjImGEli&q#)kWrc14OqS2rM3ngKb(cE z2qdg{nA~E)T666sTzuwIV@PWN)JilsH{Zn+LJ(anE@Fp`k!M3Bv=n^uL-~l3P#x}%p`Te+#6@)WN(c={NUX~6`mCs% zC7?WG!{LG;{(VUYzLH#ox7#XNqR2Bq2QtZWax#y&Gk!_qGC~tr8X8jsmSiTIjzeI9 z;w+A794ws$SDa0dg>iRx9VEEBJHdiWaCdiicLEIV!Ciw79^Bmt79hCW&bPaNV9xY= zx~uEns(vaWtgxLb>uPX6Plr=K5lmU9<7|rQ!URdWJ4= ztOPcKo+p^3(Fa+Xy2#(~67mE}=LJrfA-Sze=ysyDAHq}Q!cE2@CM>%eyC_3{Qd4bv z$#NetahLqc3I7uC@Q_1#VyjS{uTAb-PW+KVfkI2|t_y9s2ubcEi|W`n5|#N7-f%h$ z!*GjY{ZG;4!=5^Sha@5j=4fk0pyAjUN;X@KfFSl@b&rc-ymo9>guh0>*X4x7*n+w! zZ+U4sd+1@SI9oz}?%G!5R)Z=GN8$GoCl!lZL-JGn@x;>}>8OieDcUC`Yw)xz4WHI| zI+eJ{i?BKR8*$TnPXA#&RJqq5k;M-Ex&Q7toRC(gvf?ndWP}|Pp?ij`% zVIy2*?1b%o@z9}|-$&32@Iz7)5d+skO#oNZuur6_&aC$N z4A~z4YGZ2-WBCu7pQ^nhC1V+ac92yLf_Fk6#%bu=${Jlt$b)>03HD5461xz*RPij! zVTYwLEPz=_KgiH7Wrha=T237eeI~IMUMg|u&B7Y`Ymt4)cQ1Vc`L?O!s=!gb0{GL#8 zBRwfFB|~=tbir0!GY}mrB>X1coIRt0B1`FJ2`~rO@Xk;3upE2%lVI9#T|}TW6+qeC zWIu2S8r>99r~8=JJJWz#X5hIhJVLkQ5PAZ471g}^D@koB5LgZ7b&$dVge{&la&KAe zUTwIkxux(h{f_&M#+{@B%CspN;=MD$({%_Bvu*8d^S{InFI=4I>0PKPU4Xg;(y?uQ zHHu;e>1ar!X6syxb=~s`#CS(VUy?^wmbuL$_XmSoSf%zPCJ1}#nq)tazb>d$%5Xzn zqZNKipNu<|pWa@B7CJQkH)+8M9cC}1em__le(?Gpi7@%^W^*zoibg?;1CiVEuf+EQ zU=O5I3$=hZ4wF|Qb03#XbALylZB?c1pQCZeraPjPHbvm|O)@h3Ygeay#C zT(XgboD>p5nVaSB-*m;z z2~G$h=70$53~l4IQTB(m)vi)j-TdY;nr(ilxS8rjKuSN8 zAO%=zEbv4O>Y^k26?*>~zIWO@+1K##{V>Y>TV`l=>Ta@5!WV zKeFo#H{%p*_R=E6lzmCXo;&0$HsG8*IG*mg>5`WDumRDo4N;)E2yB{{fRsT_`j}=K z&!ZK(jS)xSA=}w$M~PUNbWW)tMUEKKc-UQmEOBb_`{o3VC446irw>}+Y>x(ycJIc{ zwQ;qgpCb_E3MngMk1%dSHRX^hK9R!D(bFs-ulb$;+cp|$e4|yl|Hl**FQ`7bGhwbx zx;;aVr5&0Aqp6{EeomiV(#F>Kj2)X=*11@Uwxj|BX7w|aU$9l&>P!#t`ZZoQK_a_q z1y44{<|H0Z2x8V&LWq`=h?o?4}XnnCJ9xD_bFnMPXB=cKM@&*oe zG9q6e%gSQ#ISx6T#ZrPPMHc%N409l}^1cWbE-%@+^?VuWB3QI$C}g<`Vps+mSXfwr zRexo!_7RG5j*dV{$_`U3GDqYANHhx?sGBWXaH@4Dtv+UN%0nO^6!4ul)t7Mp01*9k zihi7PcP;^7k4m-7#7l_*S!*O_=SIRAn*cbOTU`P>S= z^t+q-bVe@6|;JR(``s43qz!fYPlr`hGh+?X;B^LRNK2=4y z6#1u}tKm*_PQ-^Eh?qf&{gx?LKXMLsVYqV90 z9tR{DIGE^80yucn)hbIFM;mwu#?JnpKe-9k& zx^DIfyrMx8!SL806UQtT38~iJU=x}=7$)VIY^nLK)rpyK#{T20=@tWYXp7Qlw%Q5# zSStN^VT)Yw)`(;V>^IDQnO83$%ul)*q-ZKzJALO>cRqKfvJsW30tX4I-zC8hAPY(p z?ou(uiLu9%hM@ZJu$d@!zs}$#Z9fWzFE5G`R^`F&OwoiMzLVK!BUi>qO3W?@`gjv~ z{1v$#1d5ishJZq^0HVJ9xmc$Ox2yb9H{)84fqsA15i!Dbq_X>V0E4{L+^-7!)Y(&c z^Cm?ogoZbl-Pk(fcgGbou_rcni}vaTqD&FJDiUx~i!gCnehkB%b^Xg-DK8uvTx=9s zgtGgcyV)J5;$M-%@Od*=hr2}hPBNF1g49CIvx(xhhNXF@Ur~9oHYinRJX15XD)E=T`sFiRTaGIpu_m8lHrV z77d;Y5aU2cfr?NDBn2jGdD14q+6X8S&>Nmj`f4Iq%1-rnX>gzlAuI;wYjzmLSW7Jz zGegDE1z#cZLFOZ$GsWK#Qc`7qPg;ejA0STnCO-f8gm}fh>_eWnL39Znk)+Yl9gES= zH!29^yslisk#%k%1_lzjWQ0W*TKA+eYikrUnM&MVsEjs#*n$PP?XjXB!5C6OAiFgR zhoTO+?^#kdSVt%(r@ny-PtJ^2+r&!7ly}}@3QuH);&)t|z z6;7Kl{vmX8G7deCI?nylqLnYE#!cmX94T_``Xv`q5sI*yFymn0N-1bA=0 z0F6B>_tid5m~HcsTpu``4jU~F6t$eeP<-0q1GXq-c6)ZPoVS2}z;KTtRb5jpB*{lj5mMN8Y9*;Ro^ zBdX_9vf7oTK^4&l+Zx!*v~gG4_*1KGPB4i==p@TbJpT|%nt5Z$11pJC?2!+|+^~$^ zOX0VKsq+0PLz~pFqAV;Gb4EIqLu$W=I;T7Z9g1N!v4ll zf%};jifU%#6&axMMtEl~#4g|X0=;f2>f$s?CY?g2Y=iq3$Td! zs|UwZ{o_m30g>lb-veZ=P>}r%y*Fu><(@}MqbP-g(qFV@tx_@+pM1{@yt%)jM zpA6AW6o6h8$Z6+J0hIYr$KMeH?=Me>ib$6_{`_A!j_^R|V5L2Jcdu)h$j>rw`uB^s zUY=I;w=q77Jq*5lMvT(18v_zd2{fqAz?QeH#~7{@hj`d4QgJ!0H%q|6y8MyrACNSID|_;0%7P~%EhI?SagQl&VA-BZDYUx^~Ej6&Y+ z3L+zW*;Z5cm21nX84gGne zlJatL;U3;cWf<-nc#oTg(OZI0-WFb1yvqWO0Rg-M%d1z$GkUA?0L0;z_)Xfcr%Zoz z@C2~o^I^|9tx$b~{?3`U?&*^EDq|_b_dzD|rp==6HvYSR@;ti$+FXVJ59hM(_T17{ z4(gf`S{$R+B_^`s`s<&ejXR$VZiw*7nc);9&0+`iRCI=?W%l!KC;C6KXL@(%hJ4{S7~l`h2MiD2Mdkjd~rurj<3fs8&X;y8kOZ~9Hz5_1WIC#_EK%d z2(;C4LlG!lThYKE0wEL~_CA!c29{2M?a%L+s1b8(=@MMEQ41+WnYg_|bT5(E6*)&C zONG=4sp2YpNS|^zjK@bH1t=Do{Z0GD!SDM+aui>wCQ*-<1K13Hp-i8qmuz?uDJnJ! zMV+&5oG%<~#>)MeI?R3Yu^|?*{t_ibF!13hj8giE90voC_meRtS?}6E`8zgn%(<&h z{#ewKJm?g(A_->QXCsj&lEs*g)hEkMQV|@@%IviMx}$F^-E+chDC3Xe=Y;977m{Kq z$aZBsQuHdG&l|zMXV=fvPGY=?qv(*A%4>=d2njQun{zSSPkRXYY19Q@5_M2RX;S4b ziWIG?$CK1zKb-%KTZRKI*?jl{w(IJni5XCH$8;XxzIWb*>jaH*XI$d)y98{{pe87U z)=*LAAgROJ!n^4QDP0I`f3v9U9C;+ylEolm%AczPxm6V*Vrkb{{dkrkj0X_Z@aUj< zP!z{OrduGOGw^L+jK012Yh-x$e?c6wCO?29lufcqK5i)KoP^UQAG~=cYjHnK9;a2+ zuZqgau$aFdqc+hpX&+E$=-0N8wz5Ln5+=k+*TKA+z)*r3H)Vf%5!mw{RjShAGqOch zKF5Q<<|yb_66aKFA#c4W!@<>)pQXFrJqRQbtQ+j)W~ni<3Lhg(j(4+mh%w_U-A&xs z0=D-@tbe78^i}uX-Gk^RV_e@@0L)4O)Cw)eE2_CAlYQ(1ED%D%(U8aGhn||e%@gBU zZ<}|U6}vxitXLqlT}d(QqOkr-r7R`U)R8GRqwQZrY`B=sc|I`9aX26&!tQ9@XfM`F-an)Vu{_{<5V3{sf#VPsp-STCJ26o1d@8F-6|r8H1^Uu6I!whGj2 zKO{Vg!n02u0nJD_21KA>LT1u0_M%^@q&ZAvvM{?mvS$T%H=jiJjWOm^#uNo`^;_KOAW?>c5r( zh3#Li{y-isX<-GUF=es#7g}A2bMW+G$$oc&SQuAa?ct5FP`IF{b6MjAg4{(C@)%L% zI=uZC^)fe7W1=`aWKpFJno}-z5{1lo|89|v@xrB~PbD|O3A8EdAI_c@xy?WxSIkV! zcKrDNF9z+@KuP*HQWcd)ROKv+H^gIVegKouHe9%mtUEseAf(c)@e;GHD*f3AGg7sp z)`*@+ZNQdA9gT}yT#^?C0#IX3^_v$92-t;6RVxfjHt3RCUnuQI;193?n043(;ipR_ z^^e#kOlv9=2LByF1m4QwC^5@S)?%yfVxRDm+~vxp_aJKXn>owN>(mPm~AT?@rF z%e;Z+l77KE$dwHSt0uj_&PRt!P3B~Yk=1ICo~FdpV#FwPn6@J0VVCQD1;UVZ8J@!b zfdMc84<{~ky2rw$k%gVtJzD6=8iAOSAe@*cp&I%hdljvF3hD1pl_rQcwClpc!lCvX zPKEL3Fvq#_@Y4v>T8I|b#DC;in_{D2MDnmpWO?8PN8If>#hstEgQ-)>Tu%T0xWZXx zjK}N1f(n;yY!`1j2B0<^sya+1Ci1o6`LRnp@@l@$WMPY1cr*aSkKM!+<#*f3LiE9b zE;A~m2u21j4bV2wFpj7HYZkf0ScZePq&_pDh0|C(50w~Q+NFUeE|X_?ZkcfK;R11z zo`;eRK$S0dQ01X){s-XC0uRP%FtIFyuErFc{ZCk~+E(D(3Ro?483QTEq(!Vo&&3Jr zI2cRA5mZyIABYv4pTrOd6Qvg7{Ts~Va||C2UPb>&a{z~5mLeL=V4wrU&us}V$Vf=W z3cF(mnO8DH+ra~rTRrvSAR0wuB)Y=U(;xTntoXMr2?Yb~Hz{0b9J(RL`z`4|wyMVF zHMe1DmJ`oG^`Y2IyF(||eXM7nn!l?|F(;L!Q^agBI^ULl&2X_;{3o}IT;xIBAfO8H zcj|-(pJS~K5+YxTfGa2A`L`~(%4GnmmkE{fJGVngal9kl83Rrna*-XGg!vAou_OQa zwCQR%%6w|58V^*mj}#&l9Q;Bw+yanyEAl82@DloEQR_>GWhJttFuO)tuwLJIoHR0! zoi1t*6SmoE&189W@dpnh{Dz5M5602j!A7kmj<<#dbH|QgupRs@`=)4k>2`RLIA)Na zNW?ZJpvIl`DMH8o)aa^|_2}va*qiAijuxCq9cQKv^j6fK-5F z6I-Gp=WCEPf~cV|jGco>pzU|0tq#&N_HRwekN#u`WTm8cO-E`J{wT4=5p_=t&b>>! zH5R)EzIlzg=A9p7e#n4iu_!pb^j zu&m#Aq`1r@(BGJ101c@3ogV4qS#q|qDDXfrqbj3)nAhZYFtWCT;$GPY7Zl*i^b`lJ z_3s{+l&Y4on2c5A@POnlFxa9me(;E`xOf(=JTELi9G+8dD~Jt7#f9u9E*L+bDDt`^ zbdda1`e2FY9LF(uLddhO|61bI5AEgw9r`DBuZ#PpZk&cM%v&pKShHrrbcXuagRn~R z8PPXT)$#7h8{mf=XA@~qBo@UU)2J972ChbA1|~$`4exF&Pkf;EK`OZYYJPUM0^QMblE3lsf#D|RVS+OV?2J^|stub5!*h&p-XGZj z9v9xiWFKE8K#L*waiLtX-2L|nkfOus+deSHdamNMBJ~`8JqW$-Nq~N@5fd!M{ict!U7ZQw2u&F6e=RF4^1v7?9@2=yc7qOwFlBhs za!K=DgqnOLEa$r0nSv*{1{o&2UTUl6-650?fAp8z@szC~gjVRh6R(T`-P4c-TM4d= z;amYkUW2Kj2t=PrZc>Z)goYho zT*T2Bg_{c#w0j#wRy^q|lexHX%CPN1vZ)02ryCEQ&PAnxn-Sq=V`=tAkqi$f)qT#=t$#n?#*D<*9Y`@&^9HR-^GBP;J-~wfB&BIxzmT@ z8zuJJHxJU9yWFNLciiC#SrQB+)maZjVK-kF;38|*a?F{d2STqsyvmx@<6ud0C9RB} zf~lHhr@`U~q3V2N|2zOAhGobskbAyZnlk96$GjC1$f41Xh?tMrnjC;7I8hhs9O z*t)>GK>AQ*-B+qBg+RH!nTde!W()&82c(zZKHiQ+2aj(naVvH%M-P%j6TdtDqIpcO z9Nim?a+%^@p&(0ASx1UM#Me3nxBe8?)G+aS-OR48g8T4}s|Lg|WDYoW4o9UXrt<_( zf4fx*I5jm{OyJdgZ&$7bs@Q)@^}_@Yn}S+w7TBD&g`}u3TkS2Ub7QKi7=HXvRD7Hu zPEn+FSy1AwwAj6lCnDkiwYnaD6}le6{>z?MbH34zu5I9v?#pUAz>ODIR8fII%Z8;YE%)(k%6G3P-^DS}(QIV<8_9c8aOyKi6su6fI=W&Fd`jkbX$ z#iWk(>z{bWC|m45DR#1xUr-k@P#U_C*D3SIDRg&Y@<6Iy)U+h5%`8< zN{It7D_dSWc{$m>=UZL)+WIcASTRzL(|^KS;kbEuOFBCRc&%=(wjX=(A9qw#?9KXz z&K6WeYaKt$51)?6@OJKX|fUTVsZS_J7xlaCEfHbfr+`r1fIjJ>7> z?lHZe0;h)MY4YE4jB(uM0Ma@R8owo@tR=Ma_ERz_r3ToN?)u#xQQ2nl^1mMpo5;E& zh<+oI!--6ZjYCL7z_}xFC+xT5N+~4UvZA$KxVy5!^gi)G;&ATW0_cVat8?4)Gd3dCZqL|yT3})qf9v*dP ze12Shj%4=Um2EEmF`da5{yR8V2^Htzz!Boni%!K9q152PsZWDU$vJv|8@$FD?)%t( z&J;3i0Qn^#aQfmH$&M0MwwNtPBAc_{d|J}?QPp`31*Tct0pqv193Nr}^PMKmN^O-R z;j4m#XpRM9L-Ve@H=-eLcq-HtT2AL%9BNt{WmfC9m8J>JR{#|%k~fwfN`_#z&sCQX zv>}c78Ue~a^r44b8lRRL(q`Kn#I3oO#CO_QHLM;M@S|H0eUFOd_N&USj;XyJQJ;Mf zao|!bS+ji)f`a9c1$M9DD~8tdgCv%LZDyh4no1F4PfQ?_`1*?@MFaV4(uni5eg>Ky z&W^1wZ$5#3_*xqoiY19{{F+y}Xt3{@IQ?x9YTZ+%Q&6E{ zf~r6{{@Fivpz*Z^CD{pq10wM~HqaTk%SNCCklSzY`&!6LY|>>)S#y>#*kMnp$cQ)4 z=!wWwjW^ij%U)BB2O9ZkS|Q>o__@LqX||>2k?$fi^_{kkKv9PM`iJb{X=B#6U5=7f zQ}K}PnqL1hw~g*_v$i~tmWWj@r6Ln zG`wORmYm-_1-Du}bS?xUeI8g~q=+|P_KaE%=p>Zf-^GkP|(qjjscxDU2M z%f3Zq5bz%Jao%=DRG%0GA+|g zz_~Oa`E{E><3WW4GoS!hegFMBzM7RpuY_iqhGAS7CG%5Em?Kh|mN916(LPKh6Ss zaI3{+H>@)E~04l%m5dPxlF zO^di204qV7UG1%MmaprN^}7Ne2>CwVjfZ1!ok_qH+^|_(eaOJ)dTUiZx&@7+CVpT0 zyA7#C+AbVx2Zw;X4pWtrnx!aM8?ZmYVDCKiZ>!5qJ&oM+Z3&ox7#zdPgOxeF(5Tf@ zA8=t;)o)@k)CoHp+FEKD^nSnE>0#$MJsi=h75Fqrt~I<@h7i}jKh(0GSI#zifgD<; z`~|gV4Y`CHy>ZWM7E$Au%Ixz{1|tEJ+$g+>tM(sD<$zF)$k%=9F?>Zp!ySS=mB`#P z!}eMmp=c4uyJ{gO`RX%Mr=dV(gx|rKQvyhgYS5gce^SP6z7Ie;v7=G@>uEohk`BcG zQ`}`yGW8-E!1~@wp|CO5_@RG9ScKiL*_WALhy?!5xG(zJ?_~Y%O}J{nPt?LcdeM!1 zrSWMQW@?Wr+|0~fn%LTz5Sde$<`N;#0XDgWcyIm!Oa$2;)qn4O-(LGZ{T%!71=>d5;}}TcIiGxSyt00%%d_EO&~OEMC{B-<$ZTD|10sgm7sTR$G`Jb zxaXm326cMeY$5KTS>fYiw z+4AHW29V0ZUFGenzMU>@(FfMyHxsMTJ4P1=#*uRih5-bEYYHWeXhT?Wr-$KWuTKV8 zc;N7#V0Xfu53w=oe2Ly-jY5syps$eoBlb7SP^x#4>L9Fy zhi$`$ADBNd-JcmxZS200juq;O>o68Fb_zVwBvejX(4^3D8Jfl}%*}=PUH0(udZZ== zoSXgfDp^J?zg<*UNw9%0biwl6Zgdn|mf*|KgvS1u6fvP1CXd55TgD~-d>VCgQy$RU zF!VhI-=$@c$o9M^%%uTM2MobKYG9O|Lf3Q?SHCqggafaLw5{E;XrwrJVC(_|9FJD% z2{B{&Qnr2^AS;a6&mqo!_IOy{ts(&Ob>g(%T`)6#goaCst&dQf?a5mYfAba!F$oK& z(P9fDt}eni^QO9|(Gg;J2J*p&I-9Ltdr;bnV8t9&_on5Eb$Eo) zLc$Hd*Mh!)f1=NEm}33PJr#tkcg!_HT8pYCX;HIWjqbk-UC-`i9+_khvwrG-Uu`ti zv*1k)>@YQ5v<>L_MNQ0hHAbEE=O-ABRJS|a>T6Co`?aP4AuwS5Aqt%5{ zj#i%Q$Hbjj_QNu-?OoA)>YeBbRYkJnFTeA&JOu^!2rRh&w!*Sp^z(VfO`Yy}OxEy{ z>J5Y*$oBXTpd#}8hZxSyE_dkAHLtNoV? zSOMCP97<(Zkt=B}Zi`>@_mG20s&3Rhu-fQX6}A^+IMwdroe8Jr)kF=au3OT_`bSM>xKyKp4Wt+?7Pzlh4sTog#aR0$@R5+5DtF zC19W9g1v21Ddtv-39q%Daz&WJeum*jJli=Qx6MJV zJe$e{$q8muuNMV>MoiOeA8b;?ee8U`2L1A)GcnLIBaB&IT1r@(y1TCr5`I)Zm@=*n z6@C7#;;K$(Qi87g6&SNWRH5go-}l&aS$U}v;ep-EvR8TTG%}>jLkiCdluct-i5}Qr zgz#-HK?nu?^u$e6V8naRPawicqdb{0{#{tg8SStY*8BM;D)mSf`P)>cS3C(yJUC}X z{QR%1|Bfnx4;eSAvVRaX-;4fL{O8!(?ID~MVTSJQ67_9VcB_sBV}IQfn^^JnS?@1C z9lQRJdqb|e$Gxj1AM0WpIo%f8!6)BK5&vF0d`E|^XuoydgYNfi-i=j)9S7w1y z)=+00QJPg|nh&2hte>tUz}80ne)51jRqp!Yku{JW)aZ?vYpSamdAWNVQVQve_p|sPR0=*`{M|nvoIZad z9aX|{{xK)@AbIa%o2dqme{KUAdxFdv#WY$SjBk$orMD`WB^d-@7HkZ{P7k(DJ|90N z?}oJt%{hYm2dfS+=)@n?GqFWI1+TQbaCh7UU1rV0-+#c#C6<4G1Kd@*AdUqH>isQTFw;_4XqfJU}vsukWzab|$%<06QNh{lx? zSj#tT*q~K1TKt%RV~)1A<4Q`X94R8ebJ%35Wn6)fT5Y;QyKr4GurqM#6go6Vafg;M zJreMb70ae7Ds>xT`)XYrBXl(4ZKfN)Z08wky!VmS*`OJ2#2hK8IlFPqG@O-@+9(y% zRHpN89u+<%p@2&_zoudLA<3h=n80 z?wd&ih_$@phDdm%2A44v+lq)N4e`JAz3qIy8~xjo*C`Wq+V0+5%*q1iwLr(u%&PKv zsoiVu#lpvhU%-LLRv8hn7gBGtWy;1rBGe5OeAl4B{Tq@Jjx>SoExzS`99?`wgR{$& z9k%L>Js|9dLB{7lUH|RTF+7UML`3>qZm`$72|Z)zv^^AEaxlkyW3j;xc_TT%jf}g{ z3oN7^wtUUBImR741=_P)U%q}f#_4e4DjOPI=#%lNmb=w{Lt2Mv?YGIwcd3ZG>29Jo zw>f`q>FG+Gzf(`QE%&xBxrMn}(s&BW*eP^I%G#)?;I;h(55;VR%F}(D$^l~?cR355 z6~PN7^WKGI#yev&VOO=E?XPTT`n)ap%+2ZK+w{MkG8@^uzq{LQ+i+ADZlM(v=s~=R zne)e2F-qY1G54ZVo!0dLCT@Z4(_j0!Ms1S&wrgyJYh=U&f*VEyTMjw1KTtqlUniJk*C@@Te|NqSO#0s$ z#T_3!QrA9NkMeNy?jZt^-v+sf7sF>&`AdGT##ogn;sZwx!Oky>`bgEcEd2RUSY%gq zC$tGduNmRTKR=a=hfVWHYwrKv-B>cq8rFl8h->2vl7g4T^8cc(jsMR91l2HZbS%v_ zI=vq(cqE9BZPW1t+G%1cfa|R&wx^OEaVvIlcQBw}@9DY(@`NIuGD$Y9WOK zpz%m#g?~w|^T?o}jkA6Gv289kKTd6doxJ2WW+@x7jMzKp1@XDw3?GE*xzBi)+3K2C z7!Pw1u%t~PZZYGEo&S;*Ibtr3t&>T~6#W7(`XmlMQ9Dl~ywjccv7lbT44`SFj+qa$ zO*dKd^L^sfaoVD{(9^V6FQ?_~YkS|j19#c9`DiLH>Tg5^BThUdm>$C4s}ME4D{WGV zyuZyZqV3lrx*vB!ii2JdzV=>GcJw__67;^Y14Tb6oymU@zu0LF?z*Hlj8iSmBd(E8 zzx&=8uMjTQiT$P$EBK+hjvqkX)8G zpG{pgAfGtm^9gcQi#4)$7CvvOc*Oj)#u7nV1bDnS7;p=Rld^jgaOc?JsE3~Ca8vf( zMn#q~2lmf>Dcj?k$)Y2=@kj>dAO&$J@A zZVVrXm1EL>UxiftTL0F9du-QcVU{gMKHgui7YaVOcs;Q2as#VW=GO*p-+=Oky1OmQ zvG*fj>y$+3X}#QanPF`FuC}8m@%AWFoWE_BVGAfkAfd8&?g5 zdbbPPWBE^|pA3XH7?xx}8=l$$W$O3sqP@cnM$_;aMP zfh4Raiw{mtqfx6G9(vS8`%Hl#L%qRL9#3A^Sr_(abSY9z9jr275}?PayYv&JxX+N5 zURhH#q7z}BQi><%?(!5>ccJ&KYvhL*_&i`HBqVe^Zu|Y0CC+@l&D`4d2teAoJSTlV zC2cOs19fQ>Ol#PA0{em{H-nhI+D zMySNyCqPRz(N~W{TOlJa<_`@wn*91cG*Nn^$Qj(pp}hRBI0%*}r{111E^jom`QLLQ zNB&6v_vg)84I4A6Z)17{NS8yoz5Z>2|MJMVTLn8(oROqqyf?PrII>Q*JmB$XECuXkhUwCp!}esSC95AajzGf-YzNl>LnS8u+#e7h0MJm( zu0H<_j=d*UWuUD6k-scQrfE9}7C=%JSr45w%8&fq5FMPYCzm)mok9?K5t5j(7k_%L zz4`?A*!r?Cx!f?t5euNPISa)w7~WT!erUziz_c6p_oXxSTakL`m|peMGssD)N6S+I zxYz#zHj2ZZ3PJ|uj_1i5cbbM~FK?EORh5l}afA}fCAMtmkI~6@etOc`S}WqQ5QkW> z5g0e>_e%PVg4>r4ZKtxie^nw!J)2JD@^gY!>F4{-Cs>v7fxy1-2D`z$=v4^%pKT>H zV2AJDO7J~XA3=H<5`tVEPlpxPuGjhvV{wu5;MRF~zccmPMCZoV)omo{su>KpgraXe zpa?}53h8zFfcfYXDnlx`QqRHM>c-wXZlk(eR2UnXVfp3KSt9GuiX`$v(oG9}hld?G zB@;~jyZMtov_*M2n&*x5-nXkRh^|%#V&i z1nDgBxu0+B&c2XqIO*scDz_0yV;jWeAi;GbHCQ}_$w^lf4%K_~JKve>Ow2TnC#eq< z{Zo3igDQfVa6h)eKN2+Hwq;=9i{hxK5YB0ML|Z^QH~FQ9h8a&lln3tzXeZJr8Ding zyR|HsCgfjcs&~_a?Gm+uWnGJF^fI4m zxDqg`m+g7EE&PuvDS<6VG)M$&JZ69mOu?OJWletrCfe$LTN@kIiNKmW&id|~Sl?|! zdTh!Df`Zr0e(R51L$ZJ+-a|1r9o zuGN<3oKn365uzle$sz6aih_hy+;BKVOgYUd%25>6t~OKfu|a!PUs{RFiJU!~U}R%5 z6eY_0Ib^f}G@wR~DNE7$#HR%js`t{R3bAOb<)1Q^u2)3y69CcdJyX_MUa#n&uh37TcuxyVtcMx8a2^t z2zLla)g#~$1Yr<4WTh1zaiFQ~z1%hoh9$caDah+@8TBZWx>OWhYq{zo*8GN1Xn8wT z2@>Gtc4=5;1W$8x&-cHP=jNW4>fiTt?D1%k;7gBNi{&PS)d~vg%a-%=_U7X~1}Jtn z6y97L7sF`Lrh)Rjwkrkh3#PqWBbCGZ$A_Yd0Mu;jaf^5rx8iibLB$k_q&b%4b-o_clQYj3>bCz8gdYLoY|f0|l^`=O>%( z!nQX$!>Ca@K z^F|A{dxnRH%{uFo@K+4|n30f;F46A>$v?n8Q0<=nJVXLr4P)}KKKK%C1j8bUa*?JV z6$?LZGW(=#Y-}8-<+raBy(;#tIZPW~D8>m(2ZX^5vhMa*n}#kgN-g-QREcrSVbhk< z+g*;*r@Wt-G@l>*t5z%2IhfaAT`U|LtcE52V2`IuOMFs~CGeWTyL}PtvBz{0cMB3c zXZwLiMJcb7`qeB@AdIgvNl14jYEYGl#s&B5PfFCjZ3gph<1h9fwca9R`EBC3+VRpF zku@}N!`6-eBzc{J(^t&ozM7boqW2No45NpRp6DT(?6tAoY#gxBS`c9AYFs20zUV-O zswQzEemjbyQ6(%p#hr+ao^>&_zOI^Bob|4rgIWF#5m!%8T}PwXrt)addRFxi}oZNV!#BhdHwf!cE=g(hyK$M;SxogbaW;eC`rN( ztKnox*Wrj(%K~7?ZdI6%pOTDDe^)WDxRuM?M1Uh}b6lXEs=@v!G_5ZavZch>qr~NH z@XR9w!1sm*OTDyGhqp*-|8Kq=O7kBks0)K+U5;ZnDWu4~zE9PmNlT41>(@}x@R_9W zE1DnzZCW+C6Zd56fZt}dzl;<~%0<`xE1eV9=YM>Y8AWSC8=uo*g4Gs`bx|>7);A$s zPT^9+BTF@msu2?{WJRXJDBoEEl01p8acFi6>vci))ptTYYS#~|Fld3sy2rtA6H}HP z774A!kC&A`83Tj7_4g|+U7m*fraHVMLQOTb)Eq(fw~fAc;{P1;qn|P+&qtBq#xq3@ zq5K9eHl~ITcU4e0oFE$Z)bKf#26U=?cjIksdATg}&T&DIp;Gc>r}fNGEH2}Ju4??+ zx)-IQ2L4{b!Kt$}&B3LfeDswIvBPNXB6poXV zs+fya$+Th@`!d|;VU6ww-fPtCV}U57Y~OJWo^vi#T1~&41Y=%$(Kexv?`q*=jbFn_ zfoL6Er;#EVO3T6RmY^y z%)@kzvuj1exQNFFPS(aPs2haH(DlB5*Q6eu2-)7=X76&cHB)<~Ix6JS$yrv#hBjgy9o9i*ax`uFWfQG>sna4Cj7c^44#m9S$nWd{xB!PmGh8wEz z&;PO$On4Nd)KpvKQl!rVHR0G^QdDH3IOp=^rq(F{+35YRHn=?3fR~$+g4xQ%0#_3k z|4g6f7zYY{)lL4C;>hUFM)`AiSUKQE)qY`$$VB@sOULTfO`s2K=UsutDJ!qXgFXus zqstZ0_R7Y93;AcK?Ug68o7}f9q%4H-JMh$NfBpMLQ)>8o>1XKFh%&OatH!ipyNcpC4%JSH-$0*T=X_}xOBvm;ct zC2esdeF(6`ACEK1S4ThQu1@(peJnhgAP>8JX=R%sa#-Uh1`_yjx(v_iPBzr)g?FR( zyfb5Jqa67S&+p_i)DcYf2xsF`HiXY25{`7lyK_Z|F%)^?H~o_WZ^%TvKY@rhU>`{c z9PRBF_q(?5>%b8!EP;jCZs^Z<;bP}v6N4b8>3@ty&q#*ed$6sV=`<>qBKDbBHD`Ix zUJ3VC1A23vUEq=+5#25f*~s6UX4?cTf3gmzE~5OdI??J&BZihTYTH>sPE+F&SZY8; zoma;Gg!Jw19C2%R7?FDDL{(fvW24Ie^u&(sG>+eW82K9nSU`RPb2>X3{`JoUJ~V>` zuA({9a#kn8W#<`7>u?0@UV%d#EincgA791KctS?UxOCpzU{mlG&pSLk{0p9E*leZp zWE|Sqp<@tUKCro2-}SHaKS>HZd^D9avwr&SitM^VkYj`nw#DD!jrYByziCuQJ$lQ& zzlEyFOu3A8H*s;%aw{6o6|8j28GljFf`&(<$BR4UewBh!I6Xc69dIyz_u@VLe#uFV{XNZKUsC zXhG=_cWSttIJ7bZjs_@#-H!CYy;=Q0~KKKWG zzVdRG0h+)Dosaavl)=7RKkc#x(>kUetLoCAMm)aFcxTO@YN~!61c&`9k8BE-3fRdz z#OqaJP~}5=+PWGiB)L2iR0(0hAM0;-DyLYIxg+S$LRyMPd+j%cMf`8-~J}Txbf*zlar-k z2+;RJEl=JSIVFombI^Xd_% z!mb7!j{jbwh`oRzJE~wuVKQL{kZgu6M!hE;CGtV$rpb`d zJ1`H-O%4alPn^8|XIHNkG{!s#TH1-v$ZoSJs&P^(y7|&SVz~M@Gvre9AOcg~{_ITd zhxNV^>jrb(kBxB#u2r6ZRdSZFMQ--*^R1?w5HE-~=tF-jnz8;X&9J+9INACeW zo9dW=`;`__S2GVzg=9626HCMisD7Zr;|L)C{Fk-LA2Bv z{Kr<5AvtGJT+ zn)$I0+!#={<%h(n){k8u2^jgK!VxMvR|Fa z>n;G5hOdQwdeMJx#d(`+K{oN^65mVQIA@O<^ueLS-%ew$c+#}PXhyYmhjY50d@`+m zSXCYaAo$N2y2XEuaSPp&i?JV*6MzvXytx%>EF;NQPw(#lfzvT7pg=!)T1%t*b&?@i zyrF`JPMT<21e$9u(8ZghAGq6~2^vhu+mS<%zuS3FeZ z?~FE4?cmCXOx7~3HrV+HEi+`Co`Jy|6`&pP49w%rvcAj?Z$koI#5Ilqn2e0jD;rws zj+owCE!g+y`i!>RF<`()$L$YNxb2^lAb27)slOGlr)1` zr_`4pib7K6n^zo1<0r7d)aj(Cmc)zY34pTSm!Rc2GtC;UD;S6i37O?TpQImnG7$2z zXIVfOTLFe`0ZGWX$DMvQ#J|cVy0!CHpZ1XC$I^6f!|jw}PAKqjbd7;>bZ1xBg-DiO zcr3@|{)~^IYk7HL|7>h;QEM2i%ioE#yk zt+5gO)1H~ByHm{@!hmPkGOk@C->Y)-OiY`5QtJoT6?Cs_o~ejj__$cKkQ~GeP&dDJ zM36<5ixI-YCN3m!TdqbnA8?!x>e1L-OdpCE$r!4_tyYZIv&h}N>CM2t?f5^)WO3s+ ztEKmalGcM1P3OSpeGCq!2df zYc6Z74YGpxR2$dGxtGJhb$M}G z*+}4e&^zUtejUF3))0`PEpREU;u`h&gE`z$Z%snSKvuwHYat!i1<4-E?&6!^9IRt>kLzwtrB(lQiy*ICQ2YA((?T{K+g8mAnTMCQLa zIh8t^l(c&MI1xGeLp;*tSI9_(+$^=tdhq@#B=T*@XKCuSQqza8&^Fe+f$(_3+rZV| z+7heOp18nzRstAn?(uV;US8!3;;beuzkb+E|Hj5fEX?ZEDHgr4c`2)vlZTD i@Z|sa)-F`Kra*+NdMxzZ-5dxwY|lE{Jp0}I);|Fugc%da`$y?U{AR&qZi&*+|d@4wF8Yp*43|F`=v6SR?*jc-t48#aVJ<6C-Y5+q51 z5H<=&AA@C-AuIZf8**#|sy~W&|7DOQng7jvq39u$CiWt8;65mtACTlZ|0QiJ+_3?^ zNGBYtI<>(`0SgC0yG5bc3u*A}!diXKFMY!3>fyP)_Z3p$?!+bqK$Zo#sPZieiS z%=`S&u`2!;%41KVG_s37L$m&-W7ExpaN9aabPqY^8Q-0*kFZTl?EM+6TH)B|-Zn}I zrdA@g;#$~dW!}dLN#gg+9=;Ei>>v~^IP0&{InYBXv~IW&lH%^iyf&d|F!JL8SvcSF zhg;V>wwam#4!V#PEm13{pQx3mepl84mHB)=>2pE%($v1Y`P|ZR&IS72U}^yE8(!*u zZ#dRMce7!j?>G<7PRxCmdGK%!f))Y#f-Oz$hw6(i=*!h512x3jH@qd@zM&72Tv+fK z6e0#-w{6w_k1Q(-B6tuE9NStH7dj2n^zJwiLRuWb5)LHP$-Hpp{co7S)52EYDv>VB zC_(Xsq3i7~@tn^g(1V0yA(ZTYZ8$k_5hQ8B3p#*+V^z~uweSU94_DyKkD$>aZkMw# z3*$rz7O~m?xsX*qs=1>m9Nmdv+ZtGv922y;Pl&|iR#mk3wDetdqf8HK^CV^`LD3-0 z>hkdezkZu#mj6c4LT4cE5|ixG*i%eIRbTkoeeyj#Yv2(q4<1edE#{6{LuveJ1XBGj zVVMu8+64hkkG#Bh%j-VRRU2yo6@+PM0T}raOdY=OkZslOa+j@nkQ+&YZCS8u1%B#2 z5wu@KHmKSl&66<7R3U*Q31M%7ki&pj=xQh+oPc8+Bf1vYr>K4=YiHc9>W5j)a+Ol5 zFWPiGcD2C&J&+pUW1P=9A!29ON?W@&yyUCCM86+Kd4Y$(B``SEJSLAkP&P{Gev^e$@qFr0zOD5+ey>$KP=f%+{yJEfD-a*&ZERLVC z5Tm=6l+MhqDFmZu8*bR0!c#9H??* zTTu1jysKEIQB=aof1ss7l!{u%AFS;@gRtB~ZA z7q00Ajz|jq{EQHS+q4p-^IDf*b-xyf>%unYS%NxYpH#nG&K%xj*|m%HNL#^S!Z|lF zqBeGUY9F^sPds)ahg3BQCI|TMbCWf}tu|_L`a^9W(1R_0t1z}Wl0#J&Uj(7}vJa+@ zJoulo7MPIEKn(bNI|huxnA;kBPLV4YlA?2^m)eE?NC#i9*{&^-9LF&cO7wjw5bJKG ze+OxPMD?`yBC>6k5s3A)2MVJfs1`=wsr%!z-VcgTLpgf{W;MgDLJuC!7CeIG!NW|D zWUioC)jWLB_Ic@U9LGXX@4O|H=-px&#d%e0W&ICHxRF<2^4)a4dtgVVAV7 zxaLjZwynMBYk_IankFHfTK?AZ{$KyADV)b|-JH9B<|>MzVBMDBkyc+bGrv=3+Ehf^}1&Sohn^O8S5M zlLH6l(qd7is5J336DdUxoVsI79;>+_Xuc4fIS3XFC)h?&ig#}Ldp+C=tCnB-g0A`y z?bz^vN@4U)%P8%blQ`KmP_<5)XFKuP3KG_vl?;TUKno>id=R0EQ>`?CV50waJ(yx0 z)KacR{Y_yT6{?&mk<{|{=fDVx96^jT>wKzs?y>|p6oiWo`8o6@cRC1@m;yr!ANY|fPj(G=FZJb#|!BpKT zjO=96LE4|0f5#8AIEh;RC|6Zn&CKch$%67~J{W6R^C_C#GphMxTByYyM0DQGM-WW*bNg^c-4AWkEJF*#-xx~tZ*{mO z+JNg}R-gw{dMMuau1b0Q^+bTFN_HxUbXb)Ps^zI!Csm&-V7;PMLuu@31QLB*@jdn0 z(!+uth`lk^vHnJPqF5~bdN2gHyCZFDUtKLt{()^(|BdePWA_Y^dv4E8KLAM!oapu7 zVQE;n!q9_6lJ=S!M~U9=p!92wv=h zv7VRPwes~wB?qfoZIYjEf;_c`Wz$%vv5sc@jo>>ZDh#G)a}m z&RECBkCBdSz7vOMJ`BMVqS5xXf2?Z3mkO%}i7K6^ylB7O>e{)mm`nU1QW)EfYJLQg z=7Tup|17gC2jl&3p=m$5c8wud?zV~Csd|{EMBiZ<1=fL?1CTm7^QKVmgB{4cGTORW*M^NgQ`WKsG$>HVaSRfvaEN-+Sk1o zT;*EC82#x7!sm)wII*rxw~;TF>ZlndOA0Xlhb>2hIg0v|F}9w3nPwK+ooHcfoRu<0*U@E5_T`W zdNuv#5^KaFsg=(!OzgeIwklr=x2~*LG+Z;#YIc|_P_i=jq!(#=jLH=2Yf>wo2eUkN zZ2zf(QJFfhG2Xu69dvEDh1bYMz1}K}q*h*9&K!Art(N^qUod%0+)WF4EjQwtS@Ln5 z16326y4Cz_aO-fzv+jE)0g~+ugj@RFO=NTWXN?AZK2$A$XzS|t74t*?;fr?-P1`oG z0V@$*PkZIz{{y@VmIn`K04TM*1lU&v@nw&TKF8-jFgBA6P0-oBDWG(;b}-FQuK1$Q3%AkKjMqDhgitp z=!yyP2|5l{N3?zY`!zk%M#P(6p%!RlE5T&z+|v8FLl!lcFw>BP5U{ER_`_|V4#c|S zE?IBRazpQH)k-9yPpN9~uL?)^z$)dD8{C0vewc?WJb3v30gqsL@NfpWBliVVvO}z4 zqNidK^H%d?(0$PlgkrsGU0JSVjfm8|EDjzFxd%O7g_NI7!^ z)xtPxr3}iG`(Yb}bFL-D4NYZ@t!4(@hlHkmE;(;9(x%)=%~bsu?mY%=62s=KDj5Wp9^MpX;&IL{O!M zrUN6X74LzN;%w6BSUhz{RRcCqoOc^U<_d#R&Tu7;^AZ&wP00OzC^_&vyD6187t<}f zP@?Z$l4EaW;XX|UW)7oP9ECdtHjnEekzl`q(&Rq=H?kh2xE75}MYTY@o`2cAg_ZrS z=s1!CZxTXmmFm-LYzNWA?fy>&JJ)9pv(1K8o{~cGWuG8h2D{1Ef>ECw^@my!POkV2 z3EpV{g6=Q3vbL}neFk{Q#3N@OmHbQW(9Bs%A9#xA5wK8*@TcIjX`DIkT#{SiEih$J9XF9kr+T)HFa< zfm#i48nj08ag<5trB{n%cNC@$-k?Z2%995d*g~QSPlYvC{&+7>3d)lF?ogs{%h_7c zDOlYXN=cEHl^?bo8UOuA_yd)LiI1^Oy zZEUmL!?^;FV0rK`F9>cG(xO8zSyHYGw+!4Y9C6m#5L`7;Woi|^x0E^fbs-%-?P;<|?^`t?IVO;ZLe$EDVFPkw{EJAB6-q3Bp;5}-P@35L z6KZ*qvFL(=wIzisiK9<`kK%LsTe>fNABpA7S_8%Tb3(9oEz+{`Ws`#szcxRz>zlTb z$C+H-X9A1jsR}98Gm)3DS8ild2DVv(>>7N*eWCyUg1>HGTO;Y{Zs%Xci4^gX57&Vtf1%tqS4?sM!9nv*d zK)QS@1PXw8Lc|HRB!dCK?*}Rspi-UZxu~0V24aY`t^BKr19v?tgi{g1S)dx3*@(y< zd1y4+y7sO9M1RsOO`Iiih5B(+p@&-G^Cv#4>EWvqU0dP{A&JM{Nme3eEq}zZjQ7jB zR-Df<(64Y_Bvs5#Xb)4Xj@<6EgAX7va5-#i4%hxu-_SPC@7i$F_x!PL|6ES_vq4q^ zsN@DQb?CQboxszDjj{Te2kOS6)TR`U=}#@c+AmGV@+=VaFy#+-oHu^po(~qso_fC* z6769z@a`H9&q~l!I_ZySjrB!4>m+M#P|L(9O(D{<`ZJn8wpj>!a-qE>Nm9*nZcwil z#%_qVZvZG%`7DlryQZR5nwWcWIJxqll-Bi0NmZqV?t>&L0pXa%+}NH!i?{XvX`o|O z$f{-lN2v!rs}N=l$Y&vb`U^;!3S6=U$WFQKLaMPuLcn+q(w^-Q>sA4+5ujM&>exJX z0G3sOFVwQNb@^3$e4)0A7L2P4)w0QoE~>?GQOO=@vB_~>*0>sUwm=K7V^Dy0aZTsO zm#pz8`hi6d6DS&lS;fSGdxnfk_EYjaMDnDcBb^ZqU7Qq;#6%;EO8Ox5XctYO&MQmP ztmP1IS^G(UwA(*tl+qj^Pqsj61=NuzkUsn%+Sk4i4&`H>4bnTEV>!7W!9*Wx6wSqY zZ`A|7BdHaCTFsCAixx~CqOPH5R_b9P@CcTNXC)+VgJe;fcm}cF3tWqYIloBhalZyd zmj9$}-Amr11mnPBe5+JHM74m!kNzw2V^1R5wjM?$d)8V~it0xxeGsMWk&c#uE8gl) z(t=(-<=5)^DS~Ovd|s$+qv{YlzI~ZKQc0k;A0no4I}iblN+Xb}CPXX-Xez|d?*>wF zU{x=W%L1_kq~R%uZ~qA5$RM!q5b)X?fdEyrjCs*kB2!f1(1R^WioVe!?POfEz;hxY z*troS+rN&f!}pbQH&)WDegLN-lpP4G+u~ zqco2L0V+A9`Y*%ClRw~Uk}uqLntxowAso)`EQ~+J-wkv>xtWKHybrk2U81bRWTb8N zr)st0>oi|vQPnUH=Po>g<>A>0GB;FxsH6`e*t(M2TC;xn@j@k`mKh2pJGb0MmIP*Q zcyV98!hse{BHn!-GDD9coLb4%w8gZA7Pw(Iqn16YL7v5bpXjY1hxEzhFro#WX&o7Kl(Xv(+eRj zUj~d!K>YA$KrRC;>js90AwBpw#PcozYQ~u#^y?LZ!sIhtaZ>dF%;MOh&WmDIkm|bx zNB8^~mFzHSh+tbyUeN1~@}vQ)6(^ABIlu0Mwn)%|UMs^N>5PZs%WkWb#@|BkKie@V z+y)$Eb5^QWonFCo&u@8ZKk`G{dCGETEf&4MLz*om*0%mrFVBxW z0e`p^S|BlF(D8!6CaWM~^5$aZ&^L@y`qv7RtkYqueiTM_F|nupn5jiff`6*;>d{WL zuDJn5Y2vi=OVulO!guVOgR!0sWNENuFj>qa)_opIPaHsgbT^r@!mOm5Q(2^k1wDxD z(4)`;arWV&?}dg0uA3F8z9=H8RqrjA$M4dE39pLfVczfvmWO9CBwmz?BTtgyCkwVu z2f3A-6b!t+wg1YGOPUU+wlot>njE7KPO|qR(##;zy2cgrHq9PDu-B7O$)J=yjBsLr z7ZURNIca;e#)f>rL{rgn!HVp6jE(+s&5N=t$biy+$1nZxuT4ERlv~+DoLyTsnx@bNSN3>=2 zJ2FQf|FuJUxKr*as@zi5j0h9$$87pLvTI-$bKBw&Z_J=?W3nKeo<#QrA7#?ppu9k8 z|EhWj@vifb9)1krmX%DZ8&6Kev8X8ee596Lg2vjW(8OyMk+wBAR1c2dQqCOylIjb2 z(K{aI1I!?PN+*pP7Dact<(b|`@eltA>@OAW>O%QhJ4dbdB^sx)FDBrS_sDA?qC8`U&OG9*VJpFa#a)rwHZN{-Jtty4jVMHP?3!|q~gS&*^?x5YoC5~vI7wMK9q(6E!q^@qRdQp^)LN8fhf3b|O<}7^1^y9xA6^_Zi4UGqv`xb+W@x4P=DX^$&vUa=A8++X1|RaYE;3Wbp!>@aVX#yA+9 zU*!puC!c|;`9Bbdcegi`m`D;|-$>^MsCp2YBM*T*)}}KX8wZK6b7A6X1ft!1yp8qV zl6VS|CNo2c{#zZh5@gd-+c;}456@nB1k1y-8XO{PRgf8c5T&VuC{G)6~>>Y z391*wIyS!1;YF}PPo6?>#VWaM8_Kz(C}#(u&Zvc1`gT+`fU+?O>8T?~>^13#8qGjblw-fkIc`QCiHQx7fj-7R+>6M0%9ndzcDWO zyrAcpU=aby4nNkU1zqrUE~gLjDGafgx-nHB7ZNY3bY8;A{&&&!QT-A22&0yt8i;X4 zwvsu*#O_S@Gg&wg;h_7Iev`!PE-`RPg5nEZ5N%m~vzrUq;B!S9Cd)u_*~KVM?c?DC zIREYtc=p00SRS6`aNB2< z$wM4tLoE|pl=wx{QPm6QRSCtUJHx~Ru?0{7ns@ta40Z`=v-tsg_=D?s45 z2N)3?@)S$-T!`%MyJ1KDkXNsVTB~8QV&Jn*1ch({J+eSI9Mpm#?3FYoSSKB=rb0S8 z3OQePt7WH>O(Kh05EWYNg~jKsp;Z&wo)CN-`SB-NtgWbxT+r(_8CH!K^-yXhhfFs^ z^h_@Cp+x`r;bi~q6l0_Y+n@$p;EVP0^`clIXc550*2B5Kb+3E13-^)OB!cQ${4Y>dmsE zu63N+@sRN*zdSs9;Snqk&k1lmh^iyhIskvX6M;lGv``YE)>Su$Ti3jJvA#Ytf}i%u z&{2Z}1Fb8rq&Y0)r*=aNC0rBF`eMH%2zHd_$vD$0LSEL%l`AQe0lxoVz}J5SJTnLo z*@;F0(uUms7%(*K8i4wIZp_Vp{|eHtehZ}2WK=|b!IMc1SAPnKw5@}lXv5UrUqUU` zm@v^29<)tV9S0M#hW$dpiu@#AuNr`+!#wdA{XPiV#-0O^w9{Lr>=Z6+CRij$pKf_I z40UmJFtq}{P&=j$-V4PSX<)&G^gbaJC!Yf8aQv}O*hZxZx;v>ZUiVRW@okc@R;aoU z3Y&@omF(dK#qrSZQfr(-wS%pzK-L~wqz(RP#~Z@QfgA0mOh2CaP+(UJNcCOL!Q|YM zY(%BE?)623CRE8I-z&1KXxijx-JsYXX%B}J{h#Kn!g|1TvtJ&bz3}Rf9-b3WG#|UF z8^ts)PJ)AX3(-XT#t)F#)@*>_nJNcLFa#yoR{t+FaYY)52vTdVMqz3vqWXoZxt(GGOB0>v`$ z%mKG@McZ;5NTk1tsqVHjSzzrdpu5$bEIhU;SKs3VBqUc}kJ^8H8`ekxA{azmmhb_) zjPI5wFsb^{0~@ahTIde?VUezjPwDh|mAEzO?0y>3)^&ic0j70q8`S0^tqh5h{jsj* z1S8io8gzWusxEfNF6ZiLNbZWnX;qQGNnJ0Fmu)1bX|R1l!5+6HYY)yHsD4&$vHi%b=n-`3zTiBB5@GPymQUfXN9U z6auI{Xj#C>2rx7VL2c1c5E^N$>QBBlEpW07sagdhQAihUbt`PsIw|@+ng9%jVU0~f zz5fZQV-*NpcC{xCup%77dwm9AyHzbH0ay-cg)B&=vin{>-Hpr%1|Y^KfWw2p`sF~@ zCARK@SVFBZfmqk3c{z(O4iX%*%nN#`Wi`@A9&&w{*j;@IRTJUfW5uJ7vkd@^?vv(g zVFA?(-KN7+oPlKDs|!=lTyC4Cd-*{@V8Qn@$LADAP#AucEiE)(?o6!LnqY zV4Ytji+K0>C{8_t+~_XEIyb{8rx$nrbGIsO001BWNklwYYT(o5;@BmU60{nwUGwmqfk&`BJV)S86P4445bD?fxmw&5iuc_D z68VayKiw+24p~oP``g}x?VtZ5e{kgbyG}3L3fHafhrqY^=b>Z*(F?Ys<;u&@^U@a~ zw&gs?{x%4w1iO%dn8^T?K^2DpYUK=y*&~Q1S3p&Qu*jU9Dpez+^90!(v0gEp zT16xe&LXBSsU=9{%rOnby4TgKlV?lqBC!T9kQ}(6oEv&ye(2G^l2xs#=dfk<^P2YF z9-0e}V0maF7Wt*KDE*S)TsUg0aNb~Q#fN;6c7G$pyF`SPL@V+;@5i3ce-Qza8_l^V z?yHo}AougTaOCHAVb8}tjn*qJL)Xh*jP9FWgkb+hVkr=n90Ua@pU^%e%?#~vF>{dH zX@Pi;+XC?iAv)V3$-7RQ)RXJv>QP(4w=5l_LgG>WSEv?V>QuP0Wv^f5+BZ1jSfI0f zXaeHD?}8W`1*k$6kJS(2| zln6t%BvcFIEV7PLYptO^MKi-;AEq$kJ+9+^q-`xUe{4|;I#&}_4VByxu8vY}AV>O~ zrBG?A_C=Bd*VanoH|NdL{}PU+3ddg1#s$?fW!0>eCJ>DFyekmv4Kv|93*?HyUS(N? zJJzE(wI8ET{TLB42VQV)qpO3_N0GzlJB_xYA(Tkca^3K3Ax&9{>2NlEA~ZiVqt; z-X8672@?L%)Uisnu+FN%8XSSt(aKRc{O@V;rfOC`?@l06MJ-=&t76m&q!(AZPB}ml z&c|YqwrvI+1IQPFNC47}*Fp650C(RDOl2XJYCwAmvg4qfImnY_l%MCEg{p~kf^9=N za|B~g|CCKH7c=ZY)i<)05O8Q{g0+5Ui(E}tOsM6l`{SRAcb|WAWXtP9j$K>e69{R@ z0!}G?NHVLL)lAUOGI#>Bnuq2KBM|F?>I*N*KaQ$*(T+{X9^DC}Fv>)lqj=5=S#+zJ zwc;e1{(gmQGuR0BtlbYDo=xxwmWSp*2==}4N7^CJ>*6lwf&obW1o;lIkiB^HAf9?~2abH@UlCs5$(lX@ zwf=7O_i`qS$N%9A*!%e}q3ya0F!0ttM(2$;KsNj+Qv^+|1mPI)2NE10Y;(25onDkk zz)qT}YULQgn*I=f+fqReJ*y-K7Vo(PT>v`!UdfP%_zaH#o$WwN66kFOHm?NwyIgCB zbPm#Z1|pLN3Ps3ei~W*VU!|WA*?}s29wQWpbfT0#fEu-x?1gK;B&)(h6V<{fx7lRHSAU9fS{u^>*PaxK@X$fu9g@Ha>q+KFXL_Itz!)BNfQu z7$AjNke+_#aqRxpeb|2IkFfjR`!Sg>;CBGfd|+|W%YqAEoorPt3wwX|AP)ZQK`cA( z0<63Jz36!H3;1{FOFTXJ2rBub2t>PJ8Ea6@uDJ+Egn*V9|2JVckjgd4P{ABH-WhdP;u z$mLn(-PR*@Oq$q^EAA$GRK&ye-OcohBdy35@Oj0VWUp79Hzvgj1@Wl?9zhA`z3^@D8q|k$%n0jV!KW zQYSfyo(qs3dYFwl>2vBdJzLM6VkAu=2V?hcgIz1szY`XD63b~0_#S@ZyPUU3HVqt7 z-ONgoa5zA_k{g;VOzrzvuw^9_#ph)cdYBa+!Sc`?P`l44fx=^>-CJQ*X17ToGfi3u z1Ht6`^k9o-<^~tFOSBm2{y0x~6(;u~(S1HUSI@Xz;gUMl?}Hp_Wr|{Cj$r2x{vE&m z>c3;}eGg#DaLF0bfsico%dr-=Ku>R(#p3msQ~%(_;xV-M_d#oEfgX#Xux~Gt`EAf5 zVK~9&g~VVOfn4sPP?X~E`;X2m+;4$i8cv%+>w!x99%8iSgY7@gan(abE*PF z!)^=k=qSX#L%`J+xjBVY5ut9N45+abO41<|*7(u7{Dr{LG2pQ$fhV5?OcTPkA-Bd6 z>Rtmum8;p11n1ESNgzM+I2mVh_2TS!`$$~*DNH^CU$leI-;%Nk%Tq`$y96Vf&&6z$RDc^x(tnk>d*{Pts>h zYpb+y81SX|3wdPwZ*k9;zlKM@b0-duPcUI2m!WW-43VBErfHCE+x`1|_ND%Bt94RE zU;W`g038>ekL1M{B6;b>h@ZC!{*G42ftah-5>;6F3D_04{XqM#oy@*Q_kw}nf{ zEejc9>h_-L!tiYyenCKyf;%st5yp2T9`LJtCpZc4gilkN}WkP0-Zf6I7 zs#Z3IEY^>sII(wotvvZD)gR@`H=IW19(s7L!6R56ng%kKB=_dR$S#ChR?Q$Vc1;e* zpC{hA`J=vYYu)s4@n7`pfXyP>Zd5V{;R~hQ?bOXQ^++7hVt|#$6L)?C_k8KA*#6U> zBked$s)Ey`N>LD16p({eyJ;d)tMT5RNx>uP)}v;d+z*h$1`q2)FaXEvwj1jQ2^c z02@~^A(OuLEy%XRqHj0KV~*=ZZB4s|WbZ{7-+vb(?d!PJ<@#VXhoDIH;OSE4Alm^P zk5F0s_S^+M&r^w|^nSjlNkDH@GEJ)fxH)^|dQVxK;hyth)`|_;7JUx=(QYy^Mf&i4 z=-7Bu^E6Bp2u=}|Q~TXuY?}0KP-K;;3-#kY=VR*7eTcTN<4W0lWC07@61(b~l^lwh zLw9Jw7E6}3rdG>5JU8GGEDz0taCJcB-J=GhoOgHXml7>}L8bTg;na#NT}y=}qV63O zO!lEx97BnGu@ZfdbQzK_1*e+E17G_m{Os?(fZeHA8)h zZ!R)P#6%AypoLQ?tmp@NyMWe&>w~4LkW>v~_g+{({)OwHt}4g3 zL9NX~5r`!qz2F+x1T>#L!S12%R9>D!DSHI116R83sA_cFp3+clTqJ7;BOdgw30jgK8@?%f328%2%8s!*Ip~{38PN;n*n983-{F=^O2qxOBRh-BZTIQqnQP#WLsYM6{dV@WT!;3MqbPoy`L8gO+| z4a@_jYa4D|%c1V+L-(U~_3zYMo{hY>Bq)lXba3CS7RSG>2U}o^CdLbSc+S8hSRR@R zTI@)(l0Ebw^hgUF^MrPt!zRdWZOgB|gUas>)3lJj%C-@V_rWL+LXKPrm<8PbrGLPG z{q^UtXa51-gK(V~sj7-rMM19+=rm3EOcT@3z~S?^qU-lwjTJY)0fpUr@Qcf?cMCf8 zpHEk-=y|s1ip$XV`rk+QjW2-SzQX+lCl4!629?&y4({_gPO6$1C4|~nAh+uQ*oUi7 zUiDHqD|&#i&n2)m!_`mG)V^Pbw5ktiZE^Dm$uN{QLs2~y133RY;K~ajVnHCAJ=sf( z+fgBIfB7Sw?DlJM6zd~ z2{RINH`Fm+l3~^g?5S0lc!~`(TUK1l)3Ob1d2%}{jKbv8+=8Y1*ji$X^HjL;HA$}e z&`=1GII50`2I`6Efr_eS)!Z;*otq%2opz#rutA{dV-mcNJo!V^@<&1OKMl(z6!l?= z!lt5S-pqDNJ++_uqDs~xLC>20m(f7Q#-^QxDj z4*=?Q+{0B2OegBEBXqLg<|NudJC4n>GkM-$sf2>%PGOi4MI>WZrVU} zK{HigZI8~QX}a4ZGf~PXBqP)3T?JgW6)=rs z@iiy523@D}L2iSzt-9W=`kYD{==X`NrNR)@_o-FqO)Pjp4<-?AUCk}X;HFnLzJev# z>xrG)eL?n(YDfmsV_MI_PZp_&`vHl)KkpT+sP%?3BV zChuyPCiWpWvIFs+3pi)7G1VWJn9%ei8NB+4u(Pne@xj<+{h>$N5KIlQZA0g#m!Tom z@OrzM&PRT1_Z_kxewYkKn^Lj#@SKE4usk#!q+h7zjv|=qL#TZ%SFxxvV^^|UVjUZ9 zV|xIO(paM7I$J#mXsI?p3Sj7wU*kXj;v;zA7k4pvicrOg>0*OA@;*{w~T7N0nnPPN^K#^A#1v(ox zaKm)~Rh{xd6wMV!~EzjZsQQyvSwQhiBQ$2D;H9=;RU@`;HuKl&i~}8!f59v z80Aa@7xVyXg`-TWQhk^5z(aEu^b-8>W$?#)Fm>=gbZ&fE<1|cyVud2e9^C;lu_R(b zV(wF^W@Pv*hH1&? z%_WIOX_5^*UM1)D##a(g zhHV+pgJHzFwjeX~C{Gg8po&<6W4)y6T$#*|J^c~I9~+@c+1are@$g)MN3cAc1qf&X zTEzE2dHNUpE*e-$Nv3G`0Cr=kAs;E*)Va{%2HLV zP&KTxEhH-yWC2VPvAW_?tbYHk=zQ_Z`J*7r42Sotja-|!}0 zL@E<|P#)fo)bdMVS&eIP&IGEE_#+*xXQ7F#Nc%?4b~{Gsq)e}m`sy-A?Bs>~*d$P-Presm!EG znxm~pKcnDhGd;*7^yP^I2(|RPZrHOKnl=hV@T@9NuH}YzA`r`-;#4mIl?u?)4!rPs zVCy=Ft`;DhcEg=#*f*|T1>#5#KEw{y)Oxfki(QcB3&F>c?7ak|d;c5J(k9o3$y~gK zNve~?O7;lCEofyNwk6@ik;L3!;m5yVTqxnK|H5>FqO`_X{e(|%eL)Ks#1q7pAe-Ta&T;VRNiwt9S zZ0OMxphfW1Pri%K{lU!`OlMqT1t8$_VWT8rrD4Ey9E?#lt22SiKlu?Xf5Y3jZRyk| zVV6qvMPZ(4{PcwGkD#1B$p6kAZ*O87=LC?rn%a3(IVny)!^Vmy>1D{Ev^xc??sKyW zGg;T-bjE$FyM&jfxGL7te?{E}pxH;^ijw__W!%b}I(RQSH@|Z6t(^Mx$PI62(o5#7 z&1!L;wrQGz6*5`|u0?wP_u#Yl0+LSSbEH&4PAtRoMx@0u%kU)Xt6G(Q>q z-V0U#aUhgzs&AWz=Po>gbq;}|9!T3k=9d20vKfp&X8~!DWQAIL>k~Z}yj0eF`9(a= z$+lT5S0@h?k{dSMLP`2obrIDoHsf@lrC$Pj)Tbv8pxT|F&?NWB| zQ8sAx$9i01G!5b@Me?tbfVqy1gzZ`Y>BILU7+(hR_F1aQ>->)ct|*y&n0DQX{Lmw8 z4Pjdg_ZFi2vQil3edhq-r9{arc;n=fuS1jaP)7b9Q~O7thB}T{euQ*vT9Y40M7`xE zORTA5a1gp%9kI+3{C&3|Fz}bKs^e$xIH;;9WT;vU0qY^;xBnw*u>?_Ys_#OTd?cBg+W*q{i?nq=^wWoofAi4>@gJPS@pt?5h*AvjbW2!^n^D!$WfQ^SNCq z?1gWis?vd2?-l9cNB*EVx$ocHOW_eL4-11wu+AaSV&oWQl*XQh8c5a`=K1j?+o&Mg zw(fKBt*^V@eVSVszhZZ70ZbgYhpSIqu~;a#avaF23^mq|(&%A)?xt7b{)Zj~nJ74t zgov);JV`=dt%j@+m;x|x?NvDMpFV@g`fdCk+5Pt*n|}=jP zD?m0^d-z-v>97q5t`J%LUbXR`!r1>A)H9jzj2-AiW$*{6?E6Q=x4Z*- z?`xr2CN!yw9*r<4J&mkXfGP!`Nq)qnID%3XEtGX21?$Ao0CXMsr#^+foaJZLpCgMVFK^hu{mf9D8z~4~S5xes@an z=%e@HZ(jCl92`2zLUvnGutL{yzGcF1S{Ndsb0mT*KmAcGfAiZ}{7wPVsuE(&5=H5- z-byUIbwSfgq>mw@Gj;H{=-g0`895sua^;VrIJK7-jYRK-ZZ@5~h!zYB!^24QUVw7? zFf?Dxt)`r%DJg1IQxG^+Q)wEHqm&kL9;q@EYg>ciC%%X5(d~$LZQ*d_=4+RzIMW7W z>?bH4`~d>%{sNJ;e*{rAW{D3v_3tO#_td)A0tl3E!PGCeA^*f}NL~F)NNO0uZjfQ- zY@l|Zo`7TKkbnH$P~%$=+VBaeYJ_|3zZ5^gz2Y7`C7wVTB`8p!3I#Dl=}+jDmf>Pt z4xiAmN!pB^W?3V+)bkn|)(E#3V5Nw!oNhE*#g+PRrq(TfkpmbtYU zjPxrgr1L6gY|6a6rY(&_qBYmcY?7I8fU z`$okO@41k3^}@}JuQ|R|NJcqf2Bm#pKzQAs!!9+(Fmtx!fUHB-qR8)j7p&ZVq^`dkDp{43fv<{B;S1ukh#-Ol zk_bS9gBpw3ColyY7WQL1euE#9G7BB(!d7!DE)V`LE;{e4(0+9(a=ZQ#iHp7jSjGBx zO_M$&^uPoerOEvWC6+;vby#+BN!z2=BHgEv)Qb0y9k~13zF^CKMGt#MqaNlBk6@j1 z5U!4hj6Tg8Px;I>WnDj!l_O)?O0@gJB;yPp}6NC5MK9Q z1lq3#N)Emz{tjOjpThuFzz;tuuG|(}{onaG%RoYm8=*GehdE?%C%%jCRlbjxrrwO3 zH@q2%9iKz3=jHHq-T+ZL+#JJHA)xspC{FH$EGw=}M{Q~3iXqhpGGZOEP;&V@jdJ=O zs%3aySRUpLk6@jHAl(RQ6UZYb)V{u+4>$wp%dkAz9ZD?!Ghpd%>ne$TMHNc41+_w$ zpEjc%n_<;*GcF)Zt|Ac*Y5wm|{w4nTgCFPoKp{!!@%eBC{oFJ`3Xe!EitE4g?`XaL zMmV)JtbC3g+i6jtoWtD66WpToa|T{+NiOTu-XhN(%5fuuM$!tC^TQRq46~*6u}WG001BWNklQf+!~AnRtfnX?}Qrd zLvY=Dff}&e`3=4%{skTAVIpPYbiyr;*-lVR+&!)cqKF|5Q`GPP(m*Z0quT$$qifgU z&6!qQ^~lFzzwip)o-ncvzqiPWf%K&1>>y$to9d$W#@KWSs?9kzV(shRKED6%?^Fw; z_s9)NXz*|@z#~}aBm{r=P+Lav1tDwt>Ft?%;(j0 zpcoazT34Yu{21)&6eNwbCMWBK}a%f!=L`9hEGqC6|~uOYmDdc?SjCRU~`2Vf?^d+_IB3f0M?! z1&g_kK`Si(80 zAA>L2$-&6!!w*g6hPRNL=wvP}|5V96srK5P4D+1#-aW{s-G( zqgvL2D=JjK4iMooEZBwt+ZL=FBkK??whrM9A4C4p_rl-(8u)u&1-o*jv4GpCgJYDgODx~`22r<0SnQXgstOz~DfmrVMb)NCmV?en z9Q|?+dNTVF>e|8)dB@(KLwgm-!8ZK*3!lY5{rT_Wh90#Ow1vE+_E4n5SueqIgJu2Jj@=h0n+ z;a%TFabhn*$>k@z-7gs=MvuX%45PUF4){8*L~t3ApHre`D7P}`=jfuLLqpi}%wc{Q zD3vPwkm1u*M57_Jx2CYHvlZb)3<`PERVuJ73r;PA(26&sGWZSTAHNk^%N3Bc1caGx z%rqY9*8J@-ryfS($qysA>QA^GiHg8O_yv9|?m;Ws;ouCdFfPy4D_J&qdL4gAb0nan zs^Uo0kFRZ6g>CnI7SYZtf%dET{59%&@B>tFf(IKoTF7WdqJm%oA8lRpBGV||qWD5z zqJVF&Rp{YN@Ceqq3JO@ZZB(;^T(Q70_v<>ra;qBeBd2f6C@jtl6o9S>6bu=6j_F7n zKu960CKonA_M`4t=mF(Kw z;PV8of~vtJV(k=P^9VweN5|T@MooRmkku%P`#uM=IEd5*UxrNQ&MF@#adhuSLdZ|$ zaPLDqvG4E@48ww|)Kdb4dw)Chd6S<(Bosn-dkfaD?nhsL5A;9)R25LT`MWYNt$EKNQG zf27N;BsQhJ?LKhQcVRHv^`1idz}MxdR%p6b(ZjPI9>F??L1ud8$$ij*Dd>?lUhrfV zIa?H8^2Ts#;05+9i@C(wlIiu)ZShbdi+#dcCf9%Q(X4{33AF0sq?ip8HlDV+P#&$~ zm0dQXx`1Iy&{HXlZoeO2{M_Si^PqDjuymuS7J(obIis)Mw%Ac@?zeC2-1vOC%5yB)t`t zgI_{r_%6gR`3jU^7n~BcApIzz2t{)0ih-Fe2}Gm>sThLt*e1y1DXw0L)->$RyCK3w zNX76mlBGM8`&q;j>z{}7@BTh2d;bpMjUR_?m+JO1wW&eG^eW2g5Xz{s^}+FfF9Crn6-7r?Z5!EL@0b}Z3$o`Hc1!HqACgomReXO`!`DZ3d}~C**GDwmIjZAmS#il1>iNUAgpwmck=!^LJrHEG&SEwVvsUH5 zw+3bGElJp(Q|ba(fz8)n!;4TqRjUjG1=~h`x-IzWzlSg~uyWZMGX#lnId+wGOQFJ0a;JN%0Ge9zJEq= z;AZHlZE%dqV_%^>S*EIZ@UbV6&gPgj3E@_3ef6rI zgpkZiB{jm@F*(r9zf*kTFL(eTy7|*=ZsOP_mq&*ukSe?iyB_#1%44raNG)JQI_gIK z&=Ke+U}#W(dM)H5-4O8{M5F)_FF>@UAs_06eBXr-iVesokR8W@W0nxy_%X=-UKDn` z6OL7aq(v9?xfFH6PSsFq5mKgQvmT&M0=Lpjly?~lr)t8ZFD!YEjjSzsm(4olu+4?Q~B!95%;Jkzk zm}zXh@diBaysZdr-@yi=Gz7olb+5$czxkZo79<;n8ORk!$!O#kwS%KB{E-f>Fg2B! zNHT|Dli_H7Y&Wul48_?7+le6geaKB_vH#FfghRn&CryPN-`Ty8^4=}mz z>hAx0>)h#WXREfUt-58|k~?6GaiQA*0wg3M1qk_5UJ^n{D4`^T0153$-VjK_O9GCK z!QkM6jg5=kt=^?wZ7;La+wK47oI9(AZCSfBiw%38XKdNpojZ4KJHK7zJKe_@LR7}5nKIcln4J4h20;8*70g6iB)h6`vgDBGo3!e zl$l04W&8XnGoNe!`EUekuOGe=Al5 z-iFVYzlQsb+t4ocbLHAC1t84EyegGU+YnDoB@3bh!F>if0aba88Dgm*y!jt7`QVMv zj(r`Wfe*nI>~pS?a>8yKd~HNhYj~P>(SnYvV-w;1EtSmBO|?q;hpeqz>J=`4xePVdf_H!W8`%A+zs0dze~gZSmDu*JZvv=2i{=U* z^Gea!k4`V9k?7slBo@*$ZQ6xvnMn3rf~iAyAkw~(TdvQ_7&}XdYLJsPN)Tm@#JkOD zsr^#a;fjQ9$c^vdDL*xkm^Ze8Dh!l-*2+^JCc|#Vl-HTzwL6ty3ZcYuCe?Rsd8;SJ zo=4@3v{!n1J1RrpL2%XkphVZfu8f{Gbqhtt(C|2lc4#b^x(ODA`o zuBoyGq-!Qt$CL1f{3sO1PO!}qu`n{ZB7XGaJMgyOeFxeu`Z7j;vKiU!H$Z87HC!_b zmy#w|hZG3GF^jMY6J!}(PpC}K2NAZ1KwEALO514#AS)ss;o~vPwZX z?K7}1dD5FGbn2Boh_L80(cPd@fUoZc1SWqUg*~5yB`*iGe!woC+wq_l_Vhvc6TR?7 zyI>Zl>!#U@5#G6_hEk;Md|Pq$=nrY(AS{|O_fou=qA>{;-MDN4W>u_@?JmWl2Hjk2 zhenh&+pI_~WiaP-{6x2sIaX8s(N{+{z2OZu-JI)+D_)z`D_3`=Rm2q=K_9v53m84P z2bpp5vGZfuy7Qm~Lhw5VDmJwS0l(y_dDn(*?9hRnO}ASQqaZ5^WL1T0JD5Cr2yFxB zqaZq%n4LjiS09{W3I2^+vF!)9VdL>b+yW8(5!ksYMhZMzh^imui9IZACmq&;A(qek zI}})euEo0%Y8k-ffuADTe|a5$ac)6LiC&z7UYSMvz_p9h{x~J5B0xwBE!2+e$YV^u zx94GueF8C&8777hbBfG)n(v--Ft#C&MDHaS-*+3*hkwQber|i8O9(Oqxq!;B5 zKniMV1Fv zP0d2chfj#yiHlW58Lz-~03wWUAS4BSuWBF#F_405om(4Pk~P@TBC6Tra5^@zZglY` z`dmnokK5-)IsFT&lGF9N6ie|;(U1g7QXN!heMldSVcA7B=mlpfqhG|J61?r&>pC{P z{uWsc1zZw1oLjgMM5|UD@2Tb|xN0&Nvg9IN6>;CJjIiv$ugILx9e(r|7(K8Hk}N~7 zR$&?j)?e`oh>`@`wN6YEnzn$r=HjAY0e9OeEWhw_9DU+JREq^KF?Sra^{<2$2%uDT zF*QAiWi)r>__)0xstQjPx>gn01NS~l<;n}vK~c4Yv{#**Zt#+V#Xb*hi}g~q@-zbv zpO@F;8R3XJ?~74MqgI@N7HX>_CKhL3xUDJ5C}#(mFGNYne2kcBBDGu?gjvoa(R(qh zhGHMi9yFjK1`-G)SD-w#k0Gt+ydRWcIg}mwxzgjBIY17cRutu`m}6tz+T}PrG>TeH zhtH>S)rOKR$|q>SQ3tuhpdWXE)e)e@7r@B)UKqBCWU>d3NLR^ znBD_d%Om&LVcd54mB^S`{Pv%`3#)fsgQ-~qp{s6zbMVLTZ~P#vu62;}z}1Osaku?l z=$TfiJ_XjLk3l$pH-vJ)OR&Nvu3Vj(T*;6zgk!iEm*INxE$nYqIerr=N56&G1z$$6 z>q-Wz0>N`WwuyKz#&>)lnc>ILvf?F;nG}6qAwX1o$WQFK&vLAr%3 zHB__1=-l`Q#!%2Sv56 zOFR1cU#(11*Wyp!RSQfvawY+|ma@9|n>dYIm(i%RS$K z{{DZ0qZA?LBYVS#fi!gQQZbaJGF=Nf)>SPnotvVXQEYG5tKKxjHu-Ve27Gi>tggTO*v{!x0!J1%BNk3$p%POdD|V8ME-dpTr9MY&SNc|iw0*@b0! zR>TIawnYKgrF2Y<0>xN6RG%flJVtlF{1IjzZ0cLSZ-mf5$3uHJ7G;VOLQZdXx)l zS_e=b8pn|@|323K&Am_}tuS-rT=AvkD);#JG5F2D=Fht8gCEAx+k)_`ZMgPLB6K${J%lm`LQShAI(AUn1GN?dS8qwd#|~uCs%aU zn=mnfQG7u7D9#hEf~x?Pyk*Q_3CxFH!Y9N)}^_vglcg7i}~w!J-GRFDfGaw2k6u8M0=;sxGw&n;R~{@8ocD zA!};i5yAiWx9bw7Ewz%yX@YX*K|E?v#?{o2{P6Piw3{2`G zVyf3GaUdaUE7%Y=(BU&d8r}IW3~Aao>ioC2)9ltFHLcal8m1h z3{+DBIQCI7;cmh_bnN^QQ>O z7g8_Ia8l)2s4S#CNmGeg1p42J>hb@F!rs3|vgKk3auBZVSxnL5z%)&$vV>Q@{1W7g z6^u{LARY@tcU))$arJrEAYDyUvw3X0=wh6I!TI>X%{O6oHj8jL0$nd-)7l_b^sU5W zJ56NtAdJjoSl_Ebjkm!pdKxyW&RNw0tV)`?xm;}aF9T+`--p3}`9t)-^IK3`mh-1C z4{pbyFZ@1a4UiKuj(y*TD(uJkUw;oLSHK7)O+)VCt#GWTfJ!S?zhfF@UBo}Vy&Hvt zGMxYTTTEU5b@=nWfGwY9i*ssF<%*K4Q5?s0cpct|cM!i6)tUQIKJ-O|*8G1^TVDpJ zI^PT-DLL{yZqEidw0NczY*W`&J>N+psMYH+ETw`W%?1 zaN4J#KbwYtq~F0CJ5Bs(Qo(EuP*gwlSFlbk;#H{t|E%dQAT?q8ngZfI7h_`gtxQ2D zadT6}?t&-WrvxGx>*k8df^YP7Z@pk@H40PvnG&B``EqV~HHTL1vTq|LBDskjNG#jR z3@p-3Ev#xLa0MvTvJ&~xr?{%e?Hry;Y*vybc>1rIgYZYXxT;Ct<@}F_0M{%)3icqp z?yr$~=(kZF`Y!}mz8iMsqzN-8S=AcT++TI|W%%9??nI$TQ-vXz+`6CpIXZ*_biIl| zAb?9Ry&SjSemkbqX$-9F$J#aP0ig)NVgsPI)Zgr`m z{59LG4133NVcQWnGj^J0tiBY#`e--@i=fHlkE_r+TqwROY-1_OYJ9jXLU!TCBB_CQ z)5FAi4o=b>Bvg=HJd&32(`f~o1VkAKdeXec``HpEm2TWwn8NQRN<57O*TB^&10obW zH0#2z%IMv6A(zJuN5Dj}hGjkz*LGXz3OcY%;k3l<)XzalR!`8z+n3pRx*W!2F@uiM z6S$&p9YjrpVb}|N?ACq50ttj#S0gw2B!Y=PkNn(>az0mmLMRiZ+;I^|E^nO29cmD~tx0?CiCA=G$k$Jb`y>wF!29ao{W?^DpaUI#%e4D^jcQ#A!_n69rI`z{~@8A^Y?_a7~vNPQH|v(}=1K zB~;~#wVel!yf=dQ1;DB=)}X|zz{P`@L<_$6{y6?9VB*idIZm7kDANOQrAJQ(h@KK8 zXrI9hvPk0t!iVuj;k^KIaI_$*hrbSeYC95_|38TSPS|Ak(+I{jeb==D;+>nhf>oea zdgtaQ=q|l;sxWonsd9Gc({v5aXLx1qz35z_YP7>FH5W4LEuYzzXenOY(eMO|0M>eu z+Jqr=Ulu?$<1G3h*ROk!eL^iihCsCI6M=Zoz}d$5MXoNocp@$1_9+Dk#bw(LQB@%; zKK@lx*I^nu|G7o6FjiJ@V0;K``v+j?0=EXQYBSO4b8vUs#dN`DJ&6Jr7e#HnJY_+p zmY?NK9(wl+fp7k{gVPc&(-II>ftN)E%{rPLzr^_J&$bgX|}J;7>V)Wy4$z|n)MZS^(Y z*gIQ;G$K)?{U{^cvI?`uA4aV(4qqtMU^D6ZrdTIvqda?*SzcVxAR*L(w?n+RDg+_U z=E$@6T%J=|H~;`307*naREqL}ze8lpS74j@(*d@hSsD_21CNtl>OLkq^B30d^EmJ=)|XlZ{kAX zdcd8AGhY;wy5&qt{!KqFG&$#@WXV5t+N(|$rbBnG)iS51`)k=3c8KQe^ zB{%dX*&lW1EOO@iUbOGE`~+&*25nk+!}2N{)+9y!hrhge$&gG0?) z6=VV74<=yNcEQaKL5@+9RfOycSR1p^<#%zc?6ns=H3w}W$J43kB8s-a(^B+z0SO3_ zZ=vGy4c>C4;dcom$g)4UY}a=9GR(~sf8i!MU4cO5L# z#=a+ZV|h;(WYe=6irjEM%JD-Gx6i<+?ZeSWz6R<<2RoP!f`|#|l8im^0aWD}y2=O9 zT1%sR=5w1^RJrfK5g$jeR1VdHW|nYj8SMq2TCY$ zLM2g^m{?iRQ9%h)n8H=K1|JqagQ(C08)L8~0g~uLe)oqV2reR<{+?T!1V?RXi@VM{ z3$?CgFfZv9Q>$J9r##c3?Fh`>%8osGlV$7Qr2&?CVHxNelYKBNIn;8atPSLfIt_g| zR^88XDZ%=6i>6rR1X4bXKRS-Fhh3yL1yGr`nr3qT8yBMI>K=@Bf8H1A3|eQYUa58K z{-4iEY-16SsoWjF)Um@jw&PLg_sAdQ*cUJnm0Kb}phf0qoWRD4aWOXEsUc)dyFegjnu{ojL9f4J1o|@ zd0uFVvxaLkH9fTI1E>vs2gQA#M&j}xLy)u+#?Ppqe^jNi^Leb^IKcn?<2xUKZQBTk zg8X-LL4OYT11LHUjvqUOt=qO?V)!Uxt-TBe)$`L(wIEd8hIsW_)cRXt+`bEay?=#a z{X?kD?m}DuMgrZqt@qs+3$B6%1pzyc8;*Y!n=((JsLXvw08#P`^R*ECWpqvh_uTN? z_=`jjuFzfFVoqQk44|qYTybFtE>slp+ju*E?%sg|*oy>OpreWq!bqVFzbpP8Zos=B z!w(aKbY(#DbweM&1=XRO5xw9GkiyGhmq(hU9|U_HCVMZ%eSTynhOz&ldw9h} z^mV!kWX47?apWM@UHUTA$~s!53Zn5gwLJh>Z10W?FrbaGPQ43#*e2} z)X3sS1T>!tUqIoHrPp+fR!tl&i`Z2Zabt&xgi51`am)@q%n!t9`v$HuGz&jKQ-!2q znmTX?^wK&gYPd1>IkfmxG~}oDz!yld#;9pR`uZw8+OY}6nM25rK8;w<)`ldDEZ%ip zWJjJrq;(A=CYpwWa6R9=aB?N`Q~S9+LKuO@P`JHGP8tXx-W9#5P2Bhgy1=keDbS73 zy1zp1iT9&6{$psJuZ3MXe%g7(Nf!0T^98J0zX~@hGJbl`!we3ML_%D>noE+Y?Pi!Z z`g)c@tj?k^br?M>H^PRBvZKK)SD;D`)Zl6qazDlX2Ywgr>)#7scpb#4UC4xj`0te; z#-x8alC?3&u7R<@YCP0>BR0)G!3G<)2v{XR3Wd=72k(O#TZy;{46J$?wp0xKL*Xda z1;VgxkuBE+!Mn~)WRO57{!aKBzK3sN664s4D{zT$33|{CAC|#E1_q`fxFRGq4o9Cs zVb@2YrnbP}|0i(tthdr@g!bwgjtRB!e35np5Dd8g~nINc3iPdjNG$p~Jb5LuMF#cc#l_?*BohppN5_ojM;ZHFLoRc6f~hs0 zPhokMg`UKkVVJ1wVz?yXM^h@l2YVCx1E?1B$W3}C$B|SU0znmqu4A|=;zyG*{-9rl zTbV&&WLL_J32PJ?%eHC&W!rXmMSIW5g6&+;924{ zQEcd*z&30{dguX!+6ExWK2Esj|E3<6Av^XoCqA*R&734QjkP6s6NOs11C`t;ic<#= z>pFkoX54j5hF+S2Wfb^3viNv3t8DIJ8z^z84t*QNT_1#+dKpA*8C)wr_apUxxSsB+ zXrgyu4c-!O!OtIf9LJ82Ls2AXKAHEuSgIf%jp6*wTi{lXVR>f=fdE}QHRw_lmK203 z3MlUSDTe>?osdmn=HORY^V%u{`$C&A99)Br$`EANgf0d6I_)X%0XtUJE7?Sq=>DCL zq3xOvpV*tLj1pd!ABHrw2YTlg$blpTs>aSiRs@n(maszDf{$PeT)~7y?1j1SLa4$O z{E!e}mA$AaC_eQ`*u^7=U3o9#ESyuAVND5gv(WQAJv_GSW|U?QA=JJO&RNhk&lTh` zSWX|FC`|AFI1LiH0;R!cd5*a*FIs5vMHN(PwR>%9kr$MwoE%H>8xT!Nu)tL;38nE0 zCLh*u{s($d7_2NAjxQkSp=au4_a zZ$WA1FmKmdNOeQ-xJQhDi1)ClwqcWl&#_HPM1n0VQJC7x$ztokwQwrwc|Jf~Zd1^N zU4CLWT6!X zb49{Q*PB9wtwVPQA=9*}Ex>RJu-#hyXDq<3)v4?%y2X#bwQ*V4o9H!+%) zU7gSy@bJC7dZS7f-E3xtk0RdQg%#&t3`wuSP$HO^8bnud0*TeHcc8TA^Qey8OSS`l&uF_xsV3&@thf zFl-U2sD#Am{~$fS9{!%UK~Tdd76GA16ajx6!~gpY1p6;Q`$z7=-U#tCDL^eaetN7K^G~Z~Bf4TeL5xj!^3sWcGd))AxRi z7Y!~YPL!277F=RHIEMHC=+Bh2aN^?B8=Uq3rU9JtB>Wv$BedqDUTcuvqbaSyGlHsE zG<6&8f;@AO7kPA!&u6fO78`}By}O_WzUq(lJU^(YAhIC7Funuj>BC&bnoGEr;x{vz zmS9o2L=7pZOjetQ?Fl!ck!5Ak~%t+#gx|hgs1;wrZ*)d@|CW=)Xit59vZk3KICt2CCN8sqU zA++pDxI!Nw;NViSELath36M zfkG4zLSAxJ|H%9?9Ioz(C(m0NUeVfM7WTm!{488ogg|gCL4_z$lHi9R(T0>0f2KrI zAV!|#_gH!K_kzmSRA#LV(}_To{jgbkwMY|Alx#~r6sC3}oLYr&`vy4kFe#^?>aOey zt)Dr*eOr0v$b+7R)Y*SW&yS;$Im%4ArC5sQC0O+4exNobV8=fcaN%Et5$%;w8nHmK zk)^4^=O12)-cAH#J+GiXzN43(ccp=U$|k&mg-s<1kLP4`5NTc4QK{*u*aDN&+f)bF zv{_gYwqcvX37Z?9f*~zdybZ|gg_zk#UOCt(3)rhhF=5DTZ`0nt3Ym#f6tfvfq6EdK zVfmJek?Nr~U=@xavqmD}6Ck#3;8xV4X4`dMPITFYm^pMO6g61aHJw{kl_ntdYMO0v z5`7oflY+%uH3mjva=G1hk{#uqQvLBNKdx&d zAcvvVCXu=KV&-ebF1gi9So|c2wsH6???PpaNbZ3B$OA}RdLbeku0_1N5C8krDE1vI zV@*#h(%CG6#sn@34Z_^}Fe>8))TEdHkmlMb0F|L#h-|#Bo?lU0%g4g&(w>j=$4b89 z5lF#axE8gGm3lQ!IE{WahZ1&A+CbC=;4U=PhuNd|p*XpNjhLI;u08oZi*+5Itrrn# z9e`EKozO0xYtZ7$7fedQaL321mDyK@+SWd&QMka9f(}ZPdzY$KOK~EanqUc%0AEzV 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%0AUcLXbcjJu&+Z3rG%Uo&ZpPZlhgqqfvp=1TL)K8s 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 pWL-mIz54=D%s&x1FgpWIf Date: Tue, 8 Jul 2025 09:26:23 +1000 Subject: [PATCH 150/241] Move images to correct place. --- docs/{ => _static}/images/any.png | Bin docs/{ => _static}/images/scanpy.png | Bin docs/conf.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename docs/{ => _static}/images/any.png (100%) rename docs/{ => _static}/images/scanpy.png (100%) diff --git a/docs/images/any.png b/docs/_static/images/any.png similarity index 100% rename from docs/images/any.png rename to docs/_static/images/any.png diff --git a/docs/images/scanpy.png b/docs/_static/images/scanpy.png similarity index 100% rename from docs/images/scanpy.png rename to docs/_static/images/scanpy.png diff --git a/docs/conf.py b/docs/conf.py index 80b991b6..6ff73073 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,7 +71,7 @@ def setup(app): nbsphinx_allow_errors = True # Allow notebooks with errors nbsphinx_thumbnails = { - "tutorials/working_with_scanpy": "images/scanpy.png", + "tutorials/working_with_scanpy": "_static/images/scanpy.png", } # Autosummary From c896a555111856197e8cfe3b568e62520bafab06 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 8 Jul 2025 09:36:09 +1000 Subject: [PATCH 151/241] Fix alignment of images. --- docs/_static/css/custom.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 6beb551f..fafe36f1 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -1,5 +1,5 @@ /* Custom styling for stLearn documentation */ p img { - vertical-align: bottom + vertical-align: middle } From f6f64e676fd8627a5d0e4231af191a0fc57e988c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 8 Jul 2025 14:04:58 +1000 Subject: [PATCH 152/241] Remove anaconda stats. --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 8f318d6c..bd006a48 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,6 @@ PyPI downloads - - Conda downloads - - - Install - From 39c2f12a4440215b28bc5a1c1d5141b9fa8fd966 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 8 Jul 2025 15:24:49 +1000 Subject: [PATCH 153/241] Add edit and github to each page. --- docs/conf.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 6ff73073..7ef173a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,6 +65,23 @@ def setup(app): html_css_files = [ 'css/custom.css', ] +html_theme_options = { + "source_repository": "https://github.com/BiomedicalMachineLearning/stLearn/", + "source_branch": "master", + "source_directory": "docs/", + "footer_icons": [ + { + "name": "GitHub", + "url": "https://github.com/BiomedicalMachineLearning/stLearn/", + "html": """ + + + + """, + "class": "", + }, + ], +} # Configure nbsphinx nbsphinx_execute = 'never' # Don't re-execute notebooks From 2089d25c401a377d51ab600b2a7beb5f9f573fe9 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 12 Sep 2025 14:42:43 +1000 Subject: [PATCH 154/241] Add leiden clustering. --- stlearn/tl/clustering/__init__.py | 1 + stlearn/tl/clustering/leiden.py | 94 +++++++++++++++++++++++++++++++ stlearn/tl/clustering/louvain.py | 8 ++- 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 stlearn/tl/clustering/leiden.py diff --git a/stlearn/tl/clustering/__init__.py b/stlearn/tl/clustering/__init__.py index d68d6df2..062244fd 100644 --- a/stlearn/tl/clustering/__init__.py +++ b/stlearn/tl/clustering/__init__.py @@ -5,5 +5,6 @@ __all__ = [ "kmeans", "louvain", + "leiden", "annotate_interactive", ] diff --git a/stlearn/tl/clustering/leiden.py b/stlearn/tl/clustering/leiden.py new file mode 100644 index 00000000..50936f40 --- /dev/null +++ b/stlearn/tl/clustering/leiden.py @@ -0,0 +1,94 @@ +from collections.abc import Mapping, Sequence +from types import MappingProxyType +from typing import Any, Literal + +import scanpy +from anndata import AnnData +from louvain.VertexPartition import MutableVertexPartition +from numpy.random.mtrand import RandomState +from scipy.sparse import spmatrix + + +def louvain( + adata: AnnData, + resolution: float | None = None, + random_state: int | RandomState | None = 0, + restrict_to: tuple[str, Sequence[str]] | None = None, + key_added: str = "leiden", + adjacency: spmatrix | None = None, + directed: bool = True, + use_weights: bool = False, + partition_type: type[MutableVertexPartition] | None = None, + obsp: str | None = None, + copy: bool = False, +) -> AnnData | None: + """\ + Wrap function scanpy.tl.leiden + + This requires having ran :func:`~scanpy.pp.neighbors` or + :func:`~scanpy.external.pp.bbknn` first, + or explicitly passing a ``adjacency`` matrix. + Parameters + ---------- + adata: + The annotated data matrix. + resolution: + A parameter value controlling the coarseness of the clustering. + Higher values lead to more clusters. + Set to `None` if overriding `partition_type` + to one that doesn’t accept a `resolution_parameter`. + random_state: + Change the initialization of the optimization. + restrict_to: + Restrict the cluster to the categories within the key for sample + annotation, tuple needs to contain ``(obs_key, list_of_categories)``. + key_added: + Key under which to add the cluster labels. (default: ``'leiden'``) + adjacency: + Sparse adjacency matrix of the graph, defaults to + ``adata.uns['neighbors']['connectivities']``. + directed: + Interpret the ``adjacency`` matrix as directed graph? + use_weights: + Use weights from knn graph. + partition_type: + Type of partition to use. + Defaults to :class:`~leidenalg.RBConfigurationVertexPartition`. + For the available options, consult the documentation for + :func:`~leidenalg.find_partition`. + obsp: + Use .obsp[obsp] as adjacency. You can't specify both + `obsp` and `neighbors_key` at the same time. + copy: + Copy adata or modify it inplace. + Returns + ------- + :obj:`None` + By default (``copy=False``), updates ``adata`` with the following fields: + ``adata.obs['leiden' | key_added]`` (:class:`pandas.Series`, dtype ``category``) + Array of dim (number of samples) that stores the subgroup id + (``'0'``, ``'1'``, ...) for each cell. + :class:`~anndata.AnnData` + When ``copy=True`` is set, a copy of ``adata`` with those fields is returned. + """ + + print("Applying Leiden cluster ...") + adata = scanpy.tl.leiden( + adata, + resolution=resolution, + restrict_to=restrict_to, + random_state=random_state, + key_added=key_added, + adjacency=adjacency, + directed=directed, + use_weights=use_weights, + partition_type=partition_type, + obsp=obsp, + copy=copy, + ) + + print( + "Leiden cluster is done! The labels are stored in adata.obs['%s']" % key_added + ) + + return adata diff --git a/stlearn/tl/clustering/louvain.py b/stlearn/tl/clustering/louvain.py index 78e973dd..09c7db54 100644 --- a/stlearn/tl/clustering/louvain.py +++ b/stlearn/tl/clustering/louvain.py @@ -21,6 +21,7 @@ def louvain( use_weights: bool = False, partition_type: type[MutableVertexPartition] | None = None, partition_kwargs: Mapping[str, Any] = MappingProxyType({}), + obsp: str | None = None, copy: bool = False, ) -> AnnData | None: """\ @@ -64,6 +65,9 @@ def louvain( partition_kwargs: Key word arguments to pass to partitioning, if ``vtraag`` method is being used. + obsp: + Use .obsp[obsp] as adjacency. You can't specify both + `obsp` and `neighbors_key` at the same time. copy: Copy adata or modify it inplace. Returns @@ -77,6 +81,7 @@ def louvain( When ``copy=True`` is set, a copy of ``adata`` with those fields is returned. """ + print("Applying Louvain cluster ...") adata = scanpy.tl.louvain( adata, resolution=resolution, @@ -89,10 +94,9 @@ def louvain( use_weights=use_weights, partition_type=partition_type, partition_kwargs=partition_kwargs, + obsp=obsp, copy=copy, ) - - print("Applying Louvain cluster ...") print( "Louvain cluster is done! The labels are stored in adata.obs['%s']" % key_added ) From e4d48b47acb4e426c42e80c97cb66134a204054a Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 12:21:38 +1000 Subject: [PATCH 155/241] Fix documentation and slight refactor. --- stlearn/spatial/SME/__init__.py | 4 +- stlearn/spatial/SME/_weighting_matrix.py | 123 ++++++++---------- stlearn/spatial/SME/normalize.py | 34 +++-- .../spatial/SME/{impute.py => pseudo_spot.py} | 80 +----------- stlearn/spatial/SME/sme_impute0.py | 84 ++++++++++++ 5 files changed, 168 insertions(+), 157 deletions(-) rename stlearn/spatial/SME/{impute.py => pseudo_spot.py} (77%) create mode 100644 stlearn/spatial/SME/sme_impute0.py diff --git a/stlearn/spatial/SME/__init__.py b/stlearn/spatial/SME/__init__.py index 8fffb497..27f1dc29 100644 --- a/stlearn/spatial/SME/__init__.py +++ b/stlearn/spatial/SME/__init__.py @@ -1,8 +1,8 @@ -from .impute import SME_impute0, pseudo_spot +from .impute import pseudo_spot +from .sme_impute0 import SME_impute0 from .normalize import SME_normalize __all__ = [ "SME_normalize", - "SME_impute0", "pseudo_spot", ] diff --git a/stlearn/spatial/SME/_weighting_matrix.py b/stlearn/spatial/SME/_weighting_matrix.py index 12848161..4df84279 100644 --- a/stlearn/spatial/SME/_weighting_matrix.py +++ b/stlearn/spatial/SME/_weighting_matrix.py @@ -4,6 +4,8 @@ from anndata import AnnData from sklearn.metrics import pairwise_distances from tqdm import tqdm +from sklearn.linear_model import LinearRegression +import math _PLATFORM = Literal["Visium", "Old_ST"] _WEIGHTING_MATRIX = Literal[ @@ -17,17 +19,7 @@ ] -def calculate_weight_matrix( - adata: AnnData, - adata_imputed: AnnData | None = None, - pseudo_spots: bool = False, - platform: _PLATFORM = "Visium", -) -> AnnData | None: - import math - - from sklearn.linear_model import LinearRegression - - rate: float +def row_col_by_platform(adata, platform): if platform == "Visium": img_row = adata.obs["imagerow"] img_col = adata.obs["imagecol"] @@ -46,64 +38,61 @@ def calculate_weight_matrix( {platform!r} does not support. """ ) - reg_row = LinearRegression().fit(array_row.values.reshape(-1, 1), img_row) - reg_col = LinearRegression().fit(array_col.values.reshape(-1, 1), img_col) - - if pseudo_spots and adata_imputed: - pd = pairwise_distances( - adata_imputed.obs[["imagecol", "imagerow"]], - adata.obs[["imagecol", "imagerow"]], - metric="euclidean", - ) - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) - pd_norm = np.where(pd >= unit, 0, 1) - - md = 1 - pairwise_distances( - adata_imputed.obsm["X_morphology"], - adata.obsm["X_morphology"], - metric="cosine", - ) - md[md < 0] = 0 - - adata_imputed.uns["physical_distance"] = pd_norm - adata_imputed.uns["morphological_distance"] = md - - adata_imputed.uns["weights_matrix_all"] = ( - adata_imputed.uns["physical_distance"] - * adata_imputed.uns["morphological_distance"] - ) - - else: - pd = pairwise_distances(adata.obs[["imagecol", "imagerow"]], metric="euclidean") - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) - pd_norm = np.where(pd >= rate * unit, 0, 1) - - md = 1 - pairwise_distances(adata.obsm["X_morphology"], metric="cosine") - md[md < 0] = 0 - - gd = 1 - pairwise_distances(adata.obsm["X_pca"], metric="correlation") - adata.uns["gene_expression_correlation"] = gd - adata.uns["physical_distance"] = pd_norm - adata.uns["morphological_distance"] = md - - adata.uns["weights_matrix_all"] = ( - adata.uns["physical_distance"] - * adata.uns["morphological_distance"] - * adata.uns["gene_expression_correlation"] - ) - adata.uns["weights_matrix_pd_gd"] = ( - adata.uns["physical_distance"] * adata.uns["gene_expression_correlation"] - ) - adata.uns["weights_matrix_pd_md"] = ( - adata.uns["physical_distance"] * adata.uns["morphological_distance"] - ) - adata.uns["weights_matrix_gd_md"] = ( - adata.uns["gene_expression_correlation"] - * adata.uns["morphological_distance"] - ) - return adata + return reg_col, reg_row, rate + + +def weight_matrix(adata, platform): + reg_col, reg_row, rate = row_col_by_platform(adata, platform) + pd = pairwise_distances(adata.obs[["imagecol", "imagerow"]], metric="euclidean") + unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + pd_norm = np.where(pd >= rate * unit, 0, 1) + md = 1 - pairwise_distances(adata.obsm["X_morphology"], metric="cosine") + md[md < 0] = 0 + gd = 1 - pairwise_distances(adata.obsm["X_pca"], metric="correlation") + adata.uns["gene_expression_correlation"] = gd + adata.uns["physical_distance"] = pd_norm + adata.uns["morphological_distance"] = md + adata.uns["weights_matrix_all"] = ( + adata.uns["physical_distance"] + * adata.uns["morphological_distance"] + * adata.uns["gene_expression_correlation"] + ) + adata.uns["weights_matrix_pd_gd"] = ( + adata.uns["physical_distance"] * adata.uns["gene_expression_correlation"] + ) + adata.uns["weights_matrix_pd_md"] = ( + adata.uns["physical_distance"] * adata.uns["morphological_distance"] + ) + adata.uns["weights_matrix_gd_md"] = ( + adata.uns["gene_expression_correlation"] + * adata.uns["morphological_distance"] + ) + + +def weight_matrix_imputed(adata, adata_imputed, platform): + reg_col, reg_row, _ = row_col_by_platform(adata, platform) + + pd = pairwise_distances( + adata_imputed.obs[["imagecol", "imagerow"]], + adata.obs[["imagecol", "imagerow"]], + metric="euclidean", + ) + unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + pd_norm = np.where(pd >= unit, 0, 1) + md = 1 - pairwise_distances( + adata_imputed.obsm["X_morphology"], + adata.obsm["X_morphology"], + metric="cosine", + ) + md[md < 0] = 0 + adata_imputed.uns["physical_distance"] = pd_norm + adata_imputed.uns["morphological_distance"] = md + adata_imputed.uns["weights_matrix_all"] = ( + adata_imputed.uns["physical_distance"] + * adata_imputed.uns["morphological_distance"] + ) def impute_neighbour( diff --git a/stlearn/spatial/SME/normalize.py b/stlearn/spatial/SME/normalize.py index 39f65207..c097b390 100644 --- a/stlearn/spatial/SME/normalize.py +++ b/stlearn/spatial/SME/normalize.py @@ -6,8 +6,7 @@ from ._weighting_matrix import ( _PLATFORM, _WEIGHTING_MATRIX, - calculate_weight_matrix, - impute_neighbour, + impute_neighbour, weight_matrix, ) @@ -19,8 +18,12 @@ def SME_normalize( copy: bool = False, ) -> AnnData | None: """\ - using spatial location (S), tissue morphological feature (M) and gene - expression (E) information to normalize data. + Reduce technical noise by spatially smoothing all expression values using + spatial, morphological, and expression (SME) information. + + This function modified ALL expression values by averaging each spot's expression + with weighted contributions from similar neighbors. It modifies ALL expression + values to reduce technical noise across the entire dataset. Parameters ---------- @@ -28,19 +31,24 @@ def SME_normalize( Annotated data matrix. use_data: Input data, can be `raw` counts or log transformed data - weights: - Weighting matrix for imputation. - if `weights_matrix_all`, matrix combined all information from spatial - location (S), tissue morphological feature (M) and gene expression (E) - if `weights_matrix_pd_md`, matrix combined information from spatial - location (S), tissue morphological feature (M) + weights : _WEIGHTING_MATRIX, default="weights_matrix_all" + Strategy for computing neighbor similarity weights: + - "weights_matrix_all": Combines spatial location (S) + + morphological features (M) + gene expression correlation (E). + - "weights_matrix_pd_gd": Physical distance + gene expression correlation only. + - "weights_matrix_pd_md": Physical distance + morphological features only. + - "weights_matrix_gd_md": Gene expression + morphological features only. + - "gene_expression_correlation": Expression similarity only. + - "physical_distance": Spatial proximity only. + - "morphological_distance": Tissue morphology similarity only. platform: `Visium` or `Old_ST` copy: - Return a copy instead of writing to adata. + If True, return a copy instead of writing to adata. If False, modify adata + in place and return None. Returns ------- - Anndata + AnnData or None """ adata = adata.copy() if copy else adata @@ -60,7 +68,7 @@ def SME_normalize( else: count_embed = adata.obsm[use_data] - calculate_weight_matrix(adata, platform=platform) + weight_matrix(adata, platform=platform) impute_neighbour(adata, count_embed=count_embed, weights=weights) diff --git a/stlearn/spatial/SME/impute.py b/stlearn/spatial/SME/pseudo_spot.py similarity index 77% rename from stlearn/spatial/SME/impute.py rename to stlearn/spatial/SME/pseudo_spot.py index 68a20dc3..7151d37d 100644 --- a/stlearn/spatial/SME/impute.py +++ b/stlearn/spatial/SME/pseudo_spot.py @@ -12,79 +12,10 @@ from ._weighting_matrix import ( _PLATFORM, _WEIGHTING_MATRIX, - calculate_weight_matrix, impute_neighbour, + weight_matrix_imputed ) - -def SME_impute0( - adata: AnnData, - use_data: str = "raw", - weights: _WEIGHTING_MATRIX = "weights_matrix_all", - platform: _PLATFORM = "Visium", - copy: bool = False, -) -> AnnData | None: - """\ - using spatial location (S), tissue morphological feature (M) and gene - expression (E) information to impute missing values - - Parameters - ---------- - adata - Annotated data matrix. - use_data - input data, can be `raw` counts or log transformed data - weights - weighting matrix for imputation. - if `weights_matrix_all`, matrix combined all information from spatial - location (S), tissue morphological feature (M) and gene expression (E) - if `weights_matrix_pd_md`, matrix combined information from spatial - location (S), tissue morphological feature (M) - platform - `Visium` or `Old_ST` - copy - Return a copy instead of writing to adata. - Returns - ------- - Anndata - """ - adata = adata.copy() if copy else adata - - if use_data == "raw": - if isinstance(adata.X, csr_matrix): - count_embed = adata.X.toarray() - elif isinstance(adata.X, np.ndarray): - count_embed = adata.X - elif isinstance(adata.X, pd.Dataframe): - count_embed = adata.X.values - else: - raise ValueError( - f"""\ - {type(adata.X)} is not a valid type. - """ - ) - else: - count_embed = adata.obsm[use_data] - - calculate_weight_matrix(adata, platform=platform) - - impute_neighbour(adata, count_embed=count_embed, weights=weights) - - imputed_data = adata.obsm["imputed_data"].astype(float) - mask = count_embed != 0 - count_embed_ = count_embed.astype(float) - count_embed_[count_embed_ == 0] = np.nan - adjusted_count_matrix = np.nanmean(np.array([count_embed_, imputed_data]), axis=0) - adjusted_count_matrix[mask] = count_embed[mask] - - key_added = use_data + "_SME_imputed" - adata.obsm[key_added] = adjusted_count_matrix - - print("The data adjusted by SME is added to adata.obsm['" + key_added + "']") - - return adata if copy else None - - _COPY = Literal["pseudo_spot_adata", "combined_adata"] @@ -98,9 +29,8 @@ def pseudo_spot( copy: _COPY = "pseudo_spot_adata", ) -> AnnData | None: """\ - using spatial location (S), tissue morphological feature (M) and gene - expression (E) information to impute gap between spots and increase resolution - for gene detection + Improve spatial resolution by imputing (creating) new spots from existing ones + using spatial, morphological, and expression (SME) information. Parameters ---------- @@ -306,8 +236,8 @@ def pseudo_spot( else: count_embed = adata.obsm[use_data] - calculate_weight_matrix( - adata, pseudo_spot_adata, pseudo_spots=True, platform=platform + weight_matrix_imputed( + adata, pseudo_spot_adata, platform=platform ) impute_neighbour(pseudo_spot_adata, count_embed=count_embed, weights=weights) diff --git a/stlearn/spatial/SME/sme_impute0.py b/stlearn/spatial/SME/sme_impute0.py new file mode 100644 index 00000000..33442201 --- /dev/null +++ b/stlearn/spatial/SME/sme_impute0.py @@ -0,0 +1,84 @@ +import numpy as np +import pandas as pd +from anndata import AnnData +from scipy.sparse import csr_matrix + +from stlearn.spatial.SME._weighting_matrix import _WEIGHTING_MATRIX, _PLATFORM, \ + weight_matrix, impute_neighbour + + +def SME_impute0( + adata: AnnData, + use_data: str = "raw", + weights: _WEIGHTING_MATRIX = "weights_matrix_all", + platform: _PLATFORM = "Visium", + copy: bool = False, +) -> AnnData | None: + """\ + Fill missing/zero expression values using spatial, morphological, + and expression (SME) information when you what to correct for technical noise + (dropouts) without altering existing biological signals. + + This function replaces only zero/missing values with spatially-informed + predictions while preserving all original non-zero expression measurements. + + Parameters + ---------- + adata : + Annotated data matrix must contain obsm["X_morphology"] and obsm["X_pca"]. + use_data : + input data, can be `raw` counts or log transformed data + weights : _WEIGHTING_MATRIX, default="weights_matrix_all" + Strategy for computing neighbor similarity weights: + - "weights_matrix_all": Combines spatial location (S) + + morphological features (M) + gene expression correlation (E). + - "weights_matrix_pd_gd": Physical distance + gene expression correlation only. + - "weights_matrix_pd_md": Physical distance + morphological features only. + - "weights_matrix_gd_md": Gene expression + morphological features only. + - "gene_expression_correlation": Expression similarity only. + - "physical_distance": Spatial proximity only. + - "morphological_distance": Tissue morphology similarity only. + platform : + `Visium` or `Old_ST` + copy : + If True, return a copy instead of writing to adata. If False, modify adata + in place and return None. + Returns + ------- + AnnData or None + """ + adata = adata.copy() if copy else adata + + if use_data == "raw": + if isinstance(adata.X, csr_matrix): + count_embed = adata.X.toarray() + elif isinstance(adata.X, np.ndarray): + count_embed = adata.X + elif isinstance(adata.X, pd.Dataframe): + count_embed = adata.X.values + else: + raise ValueError( + f"""\ + {type(adata.X)} is not a valid type. + """ + ) + else: + count_embed = adata.obsm[use_data] + + weight_matrix(adata, platform=platform) + + impute_neighbour(adata, count_embed=count_embed, weights=weights) + + imputed_data = adata.obsm["imputed_data"].astype(float) + mask = count_embed != 0 + count_embed_ = count_embed.astype(float) + count_embed_[count_embed_ == 0] = np.nan + adjusted_count_matrix = np.nanmean(np.array([count_embed_, imputed_data]), axis=0) + adjusted_count_matrix[mask] = count_embed[mask] + + key_added = use_data + "_SME_imputed" + adata.obsm[key_added] = adjusted_count_matrix + + print("The data adjusted by SME is added to adata.obsm['" + key_added + "']") + + return adata if copy else None From 5b2c5458787868bc685b69e1bd5909f7fb787c45 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 12:32:11 +1000 Subject: [PATCH 156/241] Fix style. --- stlearn/spatial/SME/__init__.py | 5 +++-- stlearn/spatial/SME/_weighting_matrix.py | 11 +++++------ stlearn/spatial/SME/pseudo_spot.py | 6 ++---- stlearn/spatial/SME/sme_impute0.py | 8 ++++++-- .../spatial/SME/{normalize.py => sme_normalize.py} | 3 ++- stlearn/tl/clustering/leiden.py | 4 +--- 6 files changed, 19 insertions(+), 18 deletions(-) rename stlearn/spatial/SME/{normalize.py => sme_normalize.py} (98%) diff --git a/stlearn/spatial/SME/__init__.py b/stlearn/spatial/SME/__init__.py index 27f1dc29..988a9467 100644 --- a/stlearn/spatial/SME/__init__.py +++ b/stlearn/spatial/SME/__init__.py @@ -1,8 +1,9 @@ -from .impute import pseudo_spot +from .pseudo_spot import pseudo_spot from .sme_impute0 import SME_impute0 -from .normalize import SME_normalize +from .sme_normalize import SME_normalize __all__ = [ "SME_normalize", + "SME_impute0", "pseudo_spot", ] diff --git a/stlearn/spatial/SME/_weighting_matrix.py b/stlearn/spatial/SME/_weighting_matrix.py index 4df84279..306558d8 100644 --- a/stlearn/spatial/SME/_weighting_matrix.py +++ b/stlearn/spatial/SME/_weighting_matrix.py @@ -1,11 +1,11 @@ +import math from typing import Literal import numpy as np from anndata import AnnData +from sklearn.linear_model import LinearRegression from sklearn.metrics import pairwise_distances from tqdm import tqdm -from sklearn.linear_model import LinearRegression -import math _PLATFORM = Literal["Visium", "Old_ST"] _WEIGHTING_MATRIX = Literal[ @@ -46,7 +46,7 @@ def row_col_by_platform(adata, platform): def weight_matrix(adata, platform): reg_col, reg_row, rate = row_col_by_platform(adata, platform) pd = pairwise_distances(adata.obs[["imagecol", "imagerow"]], metric="euclidean") - unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) pd_norm = np.where(pd >= rate * unit, 0, 1) md = 1 - pairwise_distances(adata.obsm["X_morphology"], metric="cosine") md[md < 0] = 0 @@ -66,8 +66,7 @@ def weight_matrix(adata, platform): adata.uns["physical_distance"] * adata.uns["morphological_distance"] ) adata.uns["weights_matrix_gd_md"] = ( - adata.uns["gene_expression_correlation"] - * adata.uns["morphological_distance"] + adata.uns["gene_expression_correlation"] * adata.uns["morphological_distance"] ) @@ -79,7 +78,7 @@ def weight_matrix_imputed(adata, adata_imputed, platform): adata.obs[["imagecol", "imagerow"]], metric="euclidean", ) - unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) pd_norm = np.where(pd >= unit, 0, 1) md = 1 - pairwise_distances( adata_imputed.obsm["X_morphology"], diff --git a/stlearn/spatial/SME/pseudo_spot.py b/stlearn/spatial/SME/pseudo_spot.py index 7151d37d..5d154fa7 100644 --- a/stlearn/spatial/SME/pseudo_spot.py +++ b/stlearn/spatial/SME/pseudo_spot.py @@ -13,7 +13,7 @@ _PLATFORM, _WEIGHTING_MATRIX, impute_neighbour, - weight_matrix_imputed + weight_matrix_imputed, ) _COPY = Literal["pseudo_spot_adata", "combined_adata"] @@ -236,9 +236,7 @@ def pseudo_spot( else: count_embed = adata.obsm[use_data] - weight_matrix_imputed( - adata, pseudo_spot_adata, platform=platform - ) + weight_matrix_imputed(adata, pseudo_spot_adata, platform=platform) impute_neighbour(pseudo_spot_adata, count_embed=count_embed, weights=weights) diff --git a/stlearn/spatial/SME/sme_impute0.py b/stlearn/spatial/SME/sme_impute0.py index 33442201..f01ed331 100644 --- a/stlearn/spatial/SME/sme_impute0.py +++ b/stlearn/spatial/SME/sme_impute0.py @@ -3,8 +3,12 @@ from anndata import AnnData from scipy.sparse import csr_matrix -from stlearn.spatial.SME._weighting_matrix import _WEIGHTING_MATRIX, _PLATFORM, \ - weight_matrix, impute_neighbour +from stlearn.spatial.SME._weighting_matrix import ( + _PLATFORM, + _WEIGHTING_MATRIX, + impute_neighbour, + weight_matrix, +) def SME_impute0( diff --git a/stlearn/spatial/SME/normalize.py b/stlearn/spatial/SME/sme_normalize.py similarity index 98% rename from stlearn/spatial/SME/normalize.py rename to stlearn/spatial/SME/sme_normalize.py index c097b390..3fcb06d7 100644 --- a/stlearn/spatial/SME/normalize.py +++ b/stlearn/spatial/SME/sme_normalize.py @@ -6,7 +6,8 @@ from ._weighting_matrix import ( _PLATFORM, _WEIGHTING_MATRIX, - impute_neighbour, weight_matrix, + impute_neighbour, + weight_matrix, ) diff --git a/stlearn/tl/clustering/leiden.py b/stlearn/tl/clustering/leiden.py index 50936f40..f49170bc 100644 --- a/stlearn/tl/clustering/leiden.py +++ b/stlearn/tl/clustering/leiden.py @@ -1,6 +1,4 @@ -from collections.abc import Mapping, Sequence -from types import MappingProxyType -from typing import Any, Literal +from collections.abc import Sequence import scanpy from anndata import AnnData From 5ab4bb85dbfe49a6972dbf6d30878c1b31b6532e Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 12:34:18 +1000 Subject: [PATCH 157/241] Update metadata. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ddb5e570..d1705b72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.1.1" +version = "1.1.2" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] @@ -14,7 +14,7 @@ license = {text = "BSD license"} requires-python = "~=3.10.0" keywords = ["stlearn"] classifiers = [ - "Development Status :: 2 - Pre-Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", From 7447f7cee1a977095b3ae0a7a95f37ba579de589 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 12:35:28 +1000 Subject: [PATCH 158/241] Update history. --- HISTORY.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 191c3c6b..dd95dbeb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,11 @@ History ======= +1.1.2 (2025-09-17) +------------------ +* Add Leiden clustering wrapper. +* Fix documentation, refactor code in spatial.SME. + 1.1.1 (2025-07-07) ------------------ * Support Python 3.10.x From 3c1c5bbc6cbcd84f6648381af187fe84b725a85d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 13:51:43 +1000 Subject: [PATCH 159/241] Add types, fix warnings. --- requirements.txt | 3 ++- stlearn/spatial/SME/_weighting_matrix.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index eb5fccfe..6c059949 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ tensorflow==2.14.1 keras==2.14.0 types-tensorflow>=2.8.0 imageio==2.37.0 -scipy==1.11.4 \ No newline at end of file +scipy==1.11.4 +scikit-learn==1.7.0 \ No newline at end of file diff --git a/stlearn/spatial/SME/_weighting_matrix.py b/stlearn/spatial/SME/_weighting_matrix.py index 306558d8..aa15e9d7 100644 --- a/stlearn/spatial/SME/_weighting_matrix.py +++ b/stlearn/spatial/SME/_weighting_matrix.py @@ -3,7 +3,7 @@ import numpy as np from anndata import AnnData -from sklearn.linear_model import LinearRegression +from sklearn.linear_model import LinearRegression # type: ignore from sklearn.metrics import pairwise_distances from tqdm import tqdm @@ -19,7 +19,10 @@ ] -def row_col_by_platform(adata, platform): +def row_col_by_platform( + adata, platform +) -> tuple[LinearRegression, LinearRegression, float]: + rate: float if platform == "Visium": img_row = adata.obs["imagerow"] img_col = adata.obs["imagecol"] @@ -38,15 +41,16 @@ def row_col_by_platform(adata, platform): {platform!r} does not support. """ ) - reg_row = LinearRegression().fit(array_row.values.reshape(-1, 1), img_row) - reg_col = LinearRegression().fit(array_col.values.reshape(-1, 1), img_col) + regression = LinearRegression() + reg_row: LinearRegression = regression.fit(array_row.values.reshape(-1, 1), img_row) # type: ignore + reg_col: LinearRegression = regression.fit(array_col.values.reshape(-1, 1), img_col) # type: ignore return reg_col, reg_row, rate def weight_matrix(adata, platform): reg_col, reg_row, rate = row_col_by_platform(adata, platform) pd = pairwise_distances(adata.obs[["imagecol", "imagerow"]], metric="euclidean") - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) + unit = math.sqrt(reg_row.coef_[0] ** 2 + reg_col.coef_[0] ** 2) pd_norm = np.where(pd >= rate * unit, 0, 1) md = 1 - pairwise_distances(adata.obsm["X_morphology"], metric="cosine") md[md < 0] = 0 @@ -78,7 +82,7 @@ def weight_matrix_imputed(adata, adata_imputed, platform): adata.obs[["imagecol", "imagerow"]], metric="euclidean", ) - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) + unit = math.sqrt(reg_row.coef_[0] ** 2 + reg_col.coef_[0] ** 2) pd_norm = np.where(pd >= unit, 0, 1) md = 1 - pairwise_distances( adata_imputed.obsm["X_morphology"], From 502a69fd173d5b59a86a34d6d9d0de41a0e3929d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 13:52:50 +1000 Subject: [PATCH 160/241] Update version. --- docs/release_notes/1.1.2.rst | 6 ++++++ docs/release_notes/index.rst | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 docs/release_notes/1.1.2.rst diff --git a/docs/release_notes/1.1.2.rst b/docs/release_notes/1.1.2.rst new file mode 100644 index 00000000..f475d1c1 --- /dev/null +++ b/docs/release_notes/1.1.2.rst @@ -0,0 +1,6 @@ +1.1.2 `2025-09-17` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. rubric:: Features +* Add Leiden clustering wrapper. +* Fix documentation, refactor code in spatial.SME. \ No newline at end of file diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 25e378da..b57c33b4 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,6 +1,8 @@ Release Notes =================================================== +.. include:: 1.1.2.rst + .. include:: 1.1.1.rst .. include:: 0.4.6.rst From ab4f22408e5244ef1fca8025d52edff2364fada7 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 14:15:25 +1000 Subject: [PATCH 161/241] Update version again. --- CONTRIBUTING.rst | 2 -- HISTORY.rst | 2 +- docs/release_notes/{1.1.2.rst => 1.1.3.rst} | 2 +- docs/release_notes/index.rst | 2 +- pyproject.toml | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) rename docs/release_notes/{1.1.2.rst => 1.1.3.rst} (87%) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index b9769b45..4a9d9091 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -134,5 +134,3 @@ Then run:: $ bump2version patch # possible: major / minor / patch $ git push $ git push --tags - -Travis will then deploy to PyPI if tests pass. diff --git a/HISTORY.rst b/HISTORY.rst index dd95dbeb..969f302d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -1.1.2 (2025-09-17) +1.1.3 (2025-09-17) ------------------ * Add Leiden clustering wrapper. * Fix documentation, refactor code in spatial.SME. diff --git a/docs/release_notes/1.1.2.rst b/docs/release_notes/1.1.3.rst similarity index 87% rename from docs/release_notes/1.1.2.rst rename to docs/release_notes/1.1.3.rst index f475d1c1..caf435e0 100644 --- a/docs/release_notes/1.1.2.rst +++ b/docs/release_notes/1.1.3.rst @@ -1,4 +1,4 @@ -1.1.2 `2025-09-17` +1.1.3 `2025-09-17` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. rubric:: Features diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index b57c33b4..f840f187 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,7 +1,7 @@ Release Notes =================================================== -.. include:: 1.1.2.rst +.. include:: 1.1.3.rst .. include:: 1.1.1.rst diff --git a/pyproject.toml b/pyproject.toml index d1705b72..670423fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.1.2" +version = "1.1.3" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] From bf67920ba910affb86baa210356767f8766dd47e Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 14:20:47 +1000 Subject: [PATCH 162/241] Fix again! --- HISTORY.rst | 2 +- docs/release_notes/{1.1.3.rst => 1.1.4.rst} | 2 +- docs/release_notes/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename docs/release_notes/{1.1.3.rst => 1.1.4.rst} (87%) diff --git a/HISTORY.rst b/HISTORY.rst index 969f302d..4b626ca9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -1.1.3 (2025-09-17) +1.1.4 (2025-09-17) ------------------ * Add Leiden clustering wrapper. * Fix documentation, refactor code in spatial.SME. diff --git a/docs/release_notes/1.1.3.rst b/docs/release_notes/1.1.4.rst similarity index 87% rename from docs/release_notes/1.1.3.rst rename to docs/release_notes/1.1.4.rst index caf435e0..fd8c4d81 100644 --- a/docs/release_notes/1.1.3.rst +++ b/docs/release_notes/1.1.4.rst @@ -1,4 +1,4 @@ -1.1.3 `2025-09-17` +1.1.4 `2025-09-17` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. rubric:: Features diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index f840f187..8a7c5658 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,7 +1,7 @@ Release Notes =================================================== -.. include:: 1.1.3.rst +.. include:: 1.1.4.rst .. include:: 1.1.1.rst From b6e94337fc8e06d46335282812b0b5ecfbcbef28 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 14:25:10 +1000 Subject: [PATCH 163/241] . --- HISTORY.rst | 2 +- docs/release_notes/{1.1.4.rst => 1.1.5.rst} | 2 +- docs/release_notes/index.rst | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename docs/release_notes/{1.1.4.rst => 1.1.5.rst} (87%) diff --git a/HISTORY.rst b/HISTORY.rst index 4b626ca9..923e63bb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -1.1.4 (2025-09-17) +1.1.5 (2025-09-17) ------------------ * Add Leiden clustering wrapper. * Fix documentation, refactor code in spatial.SME. diff --git a/docs/release_notes/1.1.4.rst b/docs/release_notes/1.1.5.rst similarity index 87% rename from docs/release_notes/1.1.4.rst rename to docs/release_notes/1.1.5.rst index fd8c4d81..438b088c 100644 --- a/docs/release_notes/1.1.4.rst +++ b/docs/release_notes/1.1.5.rst @@ -1,4 +1,4 @@ -1.1.4 `2025-09-17` +1.1.5 `2025-09-17` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. rubric:: Features diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 8a7c5658..390df001 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,7 +1,7 @@ Release Notes =================================================== -.. include:: 1.1.4.rst +.. include:: 1.1.5.rst .. include:: 1.1.1.rst diff --git a/pyproject.toml b/pyproject.toml index 670423fc..a775e514 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.1.3" +version = "1.1.5" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] From a7a6d51822acd8435ff9e211eedfafd9185fe416 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 14:28:32 +1000 Subject: [PATCH 164/241] Missed. --- docs/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 6a7c8ee6..8ea67cc5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,8 @@ undissociated tissue sample. Latest Additions ---------------- +.. include:: release_notes/1.1.5.rst + .. include:: release_notes/1.1.1.rst .. include:: release_notes/0.4.6.rst From 3dc6816416b5061e436da7c5f619ae6a9c6997b2 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 14:30:59 +1000 Subject: [PATCH 165/241] Fix style --- stlearn/spatial/SME/sme_impute0.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/spatial/SME/sme_impute0.py b/stlearn/spatial/SME/sme_impute0.py index f01ed331..39c474da 100644 --- a/stlearn/spatial/SME/sme_impute0.py +++ b/stlearn/spatial/SME/sme_impute0.py @@ -35,7 +35,7 @@ def SME_impute0( weights : _WEIGHTING_MATRIX, default="weights_matrix_all" Strategy for computing neighbor similarity weights: - "weights_matrix_all": Combines spatial location (S) + - morphological features (M) + gene expression correlation (E). + morphological features (M) + gene expression correlation (E). - "weights_matrix_pd_gd": Physical distance + gene expression correlation only. - "weights_matrix_pd_md": Physical distance + morphological features only. - "weights_matrix_gd_md": Gene expression + morphological features only. From 533eea287eeee8e7a39630af9f65882d25840c5d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 14:33:23 +1000 Subject: [PATCH 166/241] Fix style --- stlearn/spatial/SME/sme_normalize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/spatial/SME/sme_normalize.py b/stlearn/spatial/SME/sme_normalize.py index 3fcb06d7..057578a1 100644 --- a/stlearn/spatial/SME/sme_normalize.py +++ b/stlearn/spatial/SME/sme_normalize.py @@ -35,7 +35,7 @@ def SME_normalize( weights : _WEIGHTING_MATRIX, default="weights_matrix_all" Strategy for computing neighbor similarity weights: - "weights_matrix_all": Combines spatial location (S) + - morphological features (M) + gene expression correlation (E). + morphological features (M) + gene expression correlation (E). - "weights_matrix_pd_gd": Physical distance + gene expression correlation only. - "weights_matrix_pd_md": Physical distance + morphological features only. - "weights_matrix_gd_md": Gene expression + morphological features only. From e610f431175994976aadb182e9da380d2906db8e Mon Sep 17 00:00:00 2001 From: Pedro Castillo Rosique <136706033+pedrocastillorosique@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:50:00 +0200 Subject: [PATCH 167/241] Resolve leiden.py bug As the function was called louvain, and the init.py was doing from .leiden import leiden there was a bug. --- stlearn/tl/clustering/leiden.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/tl/clustering/leiden.py b/stlearn/tl/clustering/leiden.py index f49170bc..8a721d3a 100644 --- a/stlearn/tl/clustering/leiden.py +++ b/stlearn/tl/clustering/leiden.py @@ -7,7 +7,7 @@ from scipy.sparse import spmatrix -def louvain( +def leiden( adata: AnnData, resolution: float | None = None, random_state: int | RandomState | None = 0, From 974da26fc1bac92dd8548002d1617867dd6e58b0 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 16 Oct 2025 14:32:18 +1000 Subject: [PATCH 168/241] Move imports. --- stlearn/wrapper/read.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 02f75209..4e97a2e1 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -17,7 +17,8 @@ import stlearn from stlearn.types import _BACKGROUND, _QUALITY from stlearn.wrapper.xenium_alignment import apply_alignment_transformation - +from h5py import File +from scanpy import read_csv def Read10X( path: str | Path, @@ -87,8 +88,6 @@ def Read10X( adata.uns["spatial"] = dict() - from h5py import File - with File(path / count_file, mode="r") as f: attrs = dict(f.attrs) @@ -369,7 +368,6 @@ def ReadMERFISH( coordinates = pd.read_excel(spatial_file, index_col=0) if coordinates.min().min() < 0: coordinates = coordinates + np.abs(coordinates.min().min()) + 100 - from scanpy import read_csv counts = read_csv(count_matrix_file).transpose() From ba6830145928cf6617185a0e4b21fcb7d5b172b9 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 16 Oct 2025 15:08:40 +1000 Subject: [PATCH 169/241] Update requirements and versions of python. --- CONTRIBUTING.rst | 9 ++++++++- pyproject.toml | 9 ++++++--- requirements.txt | 29 ++++++++++++++--------------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4a9d9091..720229cb 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -66,11 +66,18 @@ Ready to contribute? Here's how to set up `stlearn` for local development. 3. Install your local copy into a virtualenv. This is how you set up your fork for local development:: - $ conda create -n stlearn-dev python=3.10 --y + Run the following: + $ conda create -n stlearn-dev python=3.12 --y $ conda activate stlearn-dev $ cd stlearn/ $ pip install -e .[dev,test] + If you get an error for louvain package on MacOS, make sure you have cmake installed first (if you have brew): + $ brew install cmake + + You can also use conda to install these dependencies (after creating the environment): + $ conda install -c conda-forge louvain leidenalg python-igraph + Or if you prefer pip/virtualenv:: $ python -m venv stlearn-env diff --git a/pyproject.toml b/pyproject.toml index a775e514..2e0c6ae9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.1.5" +version = "1.1.6" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] description = "A downstream analysis toolkit for Spatial Transcriptomic data" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "BSD license"} -requires-python = "~=3.10.0" +requires-python = ">=3.10,<3.13" keywords = ["stlearn"] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -19,6 +19,8 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dynamic = ["dependencies"] @@ -34,6 +36,7 @@ dev = [ "furo==2024.8.6", "myst-parser>=0.18", "nbsphinx>=0.9.0", + "types-tensorflow>=2.8.0", "sphinx-autodoc-typehints>=1.24.0", "sphinx-autosummary-accessors>=2023.4.0", ] @@ -74,7 +77,7 @@ dependencies = {file = ["requirements.txt"]} [tool.ruff] line-length=88 -target-version = "py310" +target-version = "py312" [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP"] diff --git a/requirements.txt b/requirements.txt index 6c059949..c620378a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,14 @@ -bokeh==3.7.3 -click==8.2.1 -leidenalg==0.10.2 -louvain==0.8.2 -numba==0.58.1 -numpy==1.26.4 -pillow==11.3.0 -scanpy==1.10.4 -scikit-image==0.22.0 -tensorflow==2.14.1 -keras==2.14.0 -types-tensorflow>=2.8.0 -imageio==2.37.0 -scipy==1.11.4 -scikit-learn==1.7.0 \ No newline at end of file +bokeh>=3.7.0,<4.0 +click>=8.2.0,<9.0 +leidenalg>=0.10.0,<0.11 +louvain>=0.8.2 +numba>=0.58.1 +numpy>=1.26.0,<2.0 +pillow>=11.0.0,<12.0 +scanpy>=1.10.0,<2.0 +scikit-image>=0.22.0,<0.23 +tensorflow>=2.14.1 +keras>=2.14.0 +imageio>=2.37.0,<3.0 +scipy>=1.11.0,<2.0 +scikit-learn>=1.7.0,<2.0 \ No newline at end of file From 8699842e26e67946643f2e29f7b8c6cb57b816c2 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 16 Oct 2025 15:15:52 +1000 Subject: [PATCH 170/241] Set to minimum and fix warnings. --- pyproject.toml | 2 +- stlearn/wrapper/read.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2e0c6ae9..40cdca87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dependencies = {file = ["requirements.txt"]} [tool.ruff] line-length=88 -target-version = "py312" +target-version = "py310" [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP"] diff --git a/stlearn/wrapper/read.py b/stlearn/wrapper/read.py index 4e97a2e1..f91bf5dc 100644 --- a/stlearn/wrapper/read.py +++ b/stlearn/wrapper/read.py @@ -11,14 +11,15 @@ import pandas as pd import scanpy from anndata import AnnData +from h5py import File from matplotlib.image import imread from PIL import Image +from scanpy import read_csv import stlearn from stlearn.types import _BACKGROUND, _QUALITY from stlearn.wrapper.xenium_alignment import apply_alignment_transformation -from h5py import File -from scanpy import read_csv + def Read10X( path: str | Path, From 44987c762233a69f93be3dfdcd3f2f2ae1ce5d4c Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 16 Oct 2025 16:51:43 +1000 Subject: [PATCH 171/241] Upgrade scanpy. --- HISTORY.rst | 5 +++++ requirements.txt | 2 +- stlearn/embedding/pca.py | 6 +++--- stlearn/preprocessing/normalize.py | 18 ++---------------- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 923e63bb..f5d7da82 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,11 @@ History ======= +1.1.6 (2025-10-16) +------------------ +* Added support for Python 3.11 and 3.12. +* Upgraded scanpy to 1.11 - clustering will be different. + 1.1.5 (2025-09-17) ------------------ * Add Leiden clustering wrapper. diff --git a/requirements.txt b/requirements.txt index c620378a..67d38cd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ louvain>=0.8.2 numba>=0.58.1 numpy>=1.26.0,<2.0 pillow>=11.0.0,<12.0 -scanpy>=1.10.0,<2.0 +scanpy>=1.11.0,<2.0 scikit-image>=0.22.0,<0.23 tensorflow>=2.14.1 keras>=2.14.0 diff --git a/stlearn/embedding/pca.py b/stlearn/embedding/pca.py index 8870994e..42a2ab54 100644 --- a/stlearn/embedding/pca.py +++ b/stlearn/embedding/pca.py @@ -9,7 +9,7 @@ def run_pca( data: AnnData | np.ndarray | spmatrix, n_comps: int = 50, zero_center: bool | None = True, - svd_solver: str = "auto", + svd_solver: str = "arpack", random_state: int | RandomState | None = 0, return_info: bool = False, use_highly_variable: bool | None = None, @@ -38,11 +38,11 @@ def run_pca( Passing `None` decides automatically based on sparseness of the data. svd_solver SVD solver to use: - `'arpack'` + `'arpack'` (the default - deterministic) for the ARPACK wrapper in SciPy (:func:`~scipy.sparse.linalg.svds`) `'randomized'` for the randomized algorithm due to Halko (2009). - `'auto'` (the default) + `'auto'` chooses automatically depending on the size of the problem. random_state Change to use different initial states for the optimization. diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index 376a2f04..cb4942a7 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -12,8 +12,7 @@ def normalize_total( exclude_highly_expressed: bool = False, max_fraction: float = 0.05, key_added: str | None = None, - layers: Literal["all"] | Iterable[str] | None = None, - layer_norm: str | None = None, + layer: str | None = None, inplace: bool = True, ) -> dict[str, np.ndarray] | None: """\ @@ -48,18 +47,6 @@ def normalize_total( key_added Name of the field in `adata.obs` where the normalization factor is stored. - layers - List of layers to normalize. Set to `'all'` to normalize all layers. - layer_norm - Specifies how to normalize layers: - * If `None`, after normalization, for each layer in *layers* each cell - has a total count equal to the median of the *counts_per_cell* before - normalization of the layer. - * If `'after'`, for each layer in *layers* each cell has - a total count equal to `target_sum`. - * If `'X'`, for each layer in *layers* each cell has a total count - equal to the median of total counts for observations (cells) of - `adata.X` before normalization. inplace Whether to update `adata` or return dictionary with normalized copies of `adata.X` and `adata.layers`. @@ -76,8 +63,7 @@ def normalize_total( exclude_highly_expressed=exclude_highly_expressed, max_fraction=max_fraction, key_added=key_added, - layers=layers, - layer_norm=layer_norm, + layer=layer, inplace=inplace, ) From b1c22b288801e9a6bdf0dca9ecd1141b72f6a5e8 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 17 Oct 2025 11:10:00 +1000 Subject: [PATCH 172/241] Fix style and explicit on pandas version. --- pyproject.toml | 2 +- requirements.txt | 1 + stlearn/preprocessing/normalize.py | 3 --- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 40cdca87..46850bfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.1.6" +version = "1.2.1" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] diff --git a/requirements.txt b/requirements.txt index 67d38cd1..dfe0ccfd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ scanpy>=1.11.0,<2.0 scikit-image>=0.22.0,<0.23 tensorflow>=2.14.1 keras>=2.14.0 +pandas>=2.3.0 imageio>=2.37.0,<3.0 scipy>=1.11.0,<2.0 scikit-learn>=1.7.0,<2.0 \ No newline at end of file diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index cb4942a7..e5ecbfad 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -1,6 +1,3 @@ -from collections.abc import Iterable -from typing import Literal - import numpy as np import scanpy from anndata import AnnData From 63ed2df10069ef7ee99b38e3ef10c811b60cdaf0 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 17 Oct 2025 16:43:15 +1000 Subject: [PATCH 173/241] Add test for interaction matrix. --- tests/test_CCI.py | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/test_CCI.py b/tests/test_CCI.py index 7dc639f5..860c4da0 100644 --- a/tests/test_CCI.py +++ b/tests/test_CCI.py @@ -253,6 +253,43 @@ def test_get_interactions(self): self.assertEqual(len(observed_edgesi), len(expect_edgesi)) self.assertTrue(np.all(match_bool)) + def test_get_interaction_matrix(self): + """Test getting the interaction matrix for cell type pairs.""" + + # 3 cell types: A,E -> CT1, B,G -> CT2, C,F -> CT3 + unique_cell_type_labels = np.array(["CT1", "CT2", "CT3"]) + cell_annots = ["CT1", "CT2", "CT3", "CT2", "CT1", "CT3", "CT2"] + cell_data = np.zeros((len(cell_annots), len(unique_cell_type_labels)), + dtype=np.float64) + for i, annot in enumerate(cell_annots): + ct_index = np.where(unique_cell_type_labels == annot)[0][0] + cell_data[i, ct_index] = 1 + + # Middle spot (A) is significant and expresses ligand + sig_bool = np.array([True] + ([False] * 6)) + L_bool = sig_bool.copy() + + # Neighbors D, E, G express receptor + R_bool = np.array([False] * len(cell_annots)) + R_bool[[3, 4, 6]] = True + + # Get interaction matrix + int_matrix = het.get_interaction_matrix( + cell_data, + self.neighbourhood_bcs, + self.neighbourhood_indices, + unique_cell_type_labels, + sig_bool, + L_bool, + R_bool, + cell_prop_cutoff=0.2 + ) + + # Expected: CT1 (A) -> CT2 (D,G): 2 interactions, CT1 -> CT1 (E): 1 interaction + # Matrix is [CT1->CT1, CT1->CT2, CT1->CT3, CT2->CT1, ...] + self.assertEqual(int_matrix.shape, (3, 3)) + self.assertEqual(int_matrix[0, 0], 1) # CT1 -> CT1 (A->E) + self.assertEqual(int_matrix[0, 1], 2) # CT1 -> CT2 (A->D, A->G) + # TODO next things to test: - # 1. Getting the interaction matrix. - # 2. Getting the LR scores. + # 1. Getting the LR scores. From c88524eba5c63a38adddace1cfbdc9e518ec30ec Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 09:18:14 +1000 Subject: [PATCH 174/241] Refactor tests. --- tests/test_CCI.py | 65 ++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/tests/test_CCI.py b/tests/test_CCI.py index 860c4da0..0f9f2cbe 100644 --- a/tests/test_CCI.py +++ b/tests/test_CCI.py @@ -12,6 +12,12 @@ import stlearn.tl.cci.het_helpers as het_hs from tests.utils import read_test_data +# Per line - which cells to annotate +CELL_TYPE_ANNOTATIONS = ["CT1", "CT2", "CT3", "CT2", "CT1", "CT3", "CT2"] + +# 3 cell types: A,E -> CT1, B,G -> CT2, C,F -> CT3 +CELL_TYPE_LABELS = np.array(["CT1", "CT2", "CT3"]) + global adata adata = read_test_data() @@ -212,15 +218,17 @@ def test_get_interactions(self): 3 neighbours express receptor: * One is cell type 1, two are cell type 2. """ - cell_annots = [1, 2, 3, 2, 1, 3, 2] - cell_data = np.zeros((len(cell_annots), 3), dtype=np.float64) - for i, annot in enumerate(cell_annots): - cell_data[i, annot - 1] = 1 - all_set = np.array([str(i) for i in range(1, 4)]) - sig_bool = np.array([True] + ([False] * (len(cell_annots) - 1))) - l_bool = sig_bool - r_bool = np.array([False] * len(cell_annots)) - r_bool[[3, 4, 6]] = True + + # Create 0 matrix using the above annotations to create position. + # i.e. CT1 = 0. + cell_data = TestCCI.create_cci(CELL_TYPE_ANNOTATIONS, CELL_TYPE_LABELS) + + # Create middle ligand interacting with 3 neighbour receptors. + sig_bool = np.array([True] + ([False] * (len(CELL_TYPE_ANNOTATIONS) - 1))) + ligand_boolean = sig_bool.copy() + + receptor_boolean = np.array([False] * len(CELL_TYPE_ANNOTATIONS)) + receptor_boolean[[3, 4, 6]] = True # NOTE that format of output is an edge list for each celltype-celltype # interaction, where edge list represents interactions between: @@ -235,10 +243,10 @@ def test_get_interactions(self): cell_data, self.neighbourhood_bcs, self.neighbourhood_indices, - all_set, + CELL_TYPE_LABELS, sig_bool, - l_bool, - r_bool, + ligand_boolean, + receptor_boolean, 0, ) @@ -256,32 +264,27 @@ def test_get_interactions(self): def test_get_interaction_matrix(self): """Test getting the interaction matrix for cell type pairs.""" - # 3 cell types: A,E -> CT1, B,G -> CT2, C,F -> CT3 - unique_cell_type_labels = np.array(["CT1", "CT2", "CT3"]) - cell_annots = ["CT1", "CT2", "CT3", "CT2", "CT1", "CT3", "CT2"] - cell_data = np.zeros((len(cell_annots), len(unique_cell_type_labels)), - dtype=np.float64) - for i, annot in enumerate(cell_annots): - ct_index = np.where(unique_cell_type_labels == annot)[0][0] - cell_data[i, ct_index] = 1 + # Create 0 matrix using the above annotations to create position. + # i.e. CT1 = 0. + cell_data = TestCCI.create_cci(CELL_TYPE_ANNOTATIONS, CELL_TYPE_LABELS) # Middle spot (A) is significant and expresses ligand sig_bool = np.array([True] + ([False] * 6)) - L_bool = sig_bool.copy() + ligand_bool = sig_bool.copy() # Neighbors D, E, G express receptor - R_bool = np.array([False] * len(cell_annots)) - R_bool[[3, 4, 6]] = True + receptor_bool = np.array([False] * len(CELL_TYPE_ANNOTATIONS)) + receptor_bool[[3, 4, 6]] = True # Get interaction matrix int_matrix = het.get_interaction_matrix( cell_data, self.neighbourhood_bcs, self.neighbourhood_indices, - unique_cell_type_labels, + CELL_TYPE_LABELS, sig_bool, - L_bool, - R_bool, + ligand_bool, + receptor_bool, cell_prop_cutoff=0.2 ) @@ -290,6 +293,16 @@ def test_get_interaction_matrix(self): self.assertEqual(int_matrix.shape, (3, 3)) self.assertEqual(int_matrix[0, 0], 1) # CT1 -> CT1 (A->E) self.assertEqual(int_matrix[0, 1], 2) # CT1 -> CT2 (A->D, A->G) + self.assertEqual(int_matrix[0, 2], 0) # CT1 -> CT3 None + + @staticmethod + def create_cci(cell_annotations: list[str], unique_cell_type_labels): + cell_data = np.zeros((len(cell_annotations), len(unique_cell_type_labels)), + dtype=np.float64) + for i, annot in enumerate(cell_annotations): + ct_index = np.where(unique_cell_type_labels == annot)[0][0] + cell_data[i, ct_index] = 1 + return cell_data # TODO next things to test: # 1. Getting the LR scores. From 3cda78b2de34283419f5dc5a2f2f2f5cc132c0f7 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 15:37:53 +1000 Subject: [PATCH 175/241] Update to 3.12. --- .github/workflows/python-package.yml | 2 +- .readthedocs.yml | 2 +- HISTORY.rst | 3 ++- docs/installation.rst | 2 +- docs/release_notes/1.2.0.rst | 7 +++++++ docs/release_notes/index.rst | 2 ++ pyproject.toml | 2 +- 7 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 docs/release_notes/1.2.0.rst diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index a0935c6d..aedd3d86 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.10.18] + python-version: [3.12.12] steps: - uses: actions/checkout@v2 diff --git a/.readthedocs.yml b/.readthedocs.yml index e841d344..8c918f7a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,7 +8,7 @@ version: 2 build: os: ubuntu-24.04 tools: - python: "3.10" + python: "3.12" # Build documentation in the "docs/" directory with Sphinx sphinx: diff --git a/HISTORY.rst b/HISTORY.rst index f5d7da82..2ed1f11a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,10 +2,11 @@ History ======= -1.1.6 (2025-10-16) +1.2.0 (2025-10-20) ------------------ * Added support for Python 3.11 and 3.12. * Upgraded scanpy to 1.11 - clustering will be different. +* Added more CCI tests. 1.1.5 (2025-09-17) ------------------ diff --git a/docs/installation.rst b/docs/installation.rst index f46d62f8..93f8beda 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -13,7 +13,7 @@ Install by PyPi Prepare conda environment for stLearn :: - conda create -n stlearn python=3.10 --y + conda create -n stlearn python=3.12 --y conda activate stlearn **Step 2:** diff --git a/docs/release_notes/1.2.0.rst b/docs/release_notes/1.2.0.rst new file mode 100644 index 00000000..4fde7e01 --- /dev/null +++ b/docs/release_notes/1.2.0.rst @@ -0,0 +1,7 @@ +1.1.5 `2025-10-20` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. rubric:: Features +* Added support for Python 3.11 and 3.12. +* Upgraded scanpy to 1.11 - clustering will be different. +* Added more CCI tests. \ No newline at end of file diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 390df001..c3d4edff 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,6 +1,8 @@ Release Notes =================================================== +.. include:: 1.2.0.rst + .. include:: 1.1.5.rst .. include:: 1.1.1.rst diff --git a/pyproject.toml b/pyproject.toml index 46850bfe..c828cc9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.2.1" +version = "1.2.0" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] From d951ee950ca6c0a91b1b4cb4cdd94d0ae93592f6 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 15:59:36 +1000 Subject: [PATCH 176/241] Add cmake and build tools. --- .readthedocs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 8c918f7a..132ae893 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,6 +9,9 @@ build: os: ubuntu-24.04 tools: python: "3.12" + apt_packages: + - cmake + - build-essential # Build documentation in the "docs/" directory with Sphinx sphinx: From 972c163f63f79bd2cdfc99cfef69e14198e62c09 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 16:31:29 +1000 Subject: [PATCH 177/241] Update to 1.2.1. --- HISTORY.rst | 2 +- docs/release_notes/{1.2.0.rst => 1.2.1.rst} | 0 docs/release_notes/index.rst | 2 +- pyproject.toml | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename docs/release_notes/{1.2.0.rst => 1.2.1.rst} (100%) diff --git a/HISTORY.rst b/HISTORY.rst index 2ed1f11a..09701f34 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -1.2.0 (2025-10-20) +1.2.1 (2025-10-20) ------------------ * Added support for Python 3.11 and 3.12. * Upgraded scanpy to 1.11 - clustering will be different. diff --git a/docs/release_notes/1.2.0.rst b/docs/release_notes/1.2.1.rst similarity index 100% rename from docs/release_notes/1.2.0.rst rename to docs/release_notes/1.2.1.rst diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index c3d4edff..9ed52738 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,7 +1,7 @@ Release Notes =================================================== -.. include:: 1.2.0.rst +.. include:: 1.2.1.rst .. include:: 1.1.5.rst diff --git a/pyproject.toml b/pyproject.toml index c828cc9f..46850bfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.2.0" +version = "1.2.1" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] From 682c7bd22f00f8bbcb38f30f98fa6b1fd359faf8 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 16:38:39 +1000 Subject: [PATCH 178/241] Update to 1.2.1. --- docs/release_notes/1.2.1.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release_notes/1.2.1.rst b/docs/release_notes/1.2.1.rst index 4fde7e01..0bbff556 100644 --- a/docs/release_notes/1.2.1.rst +++ b/docs/release_notes/1.2.1.rst @@ -4,4 +4,7 @@ .. rubric:: Features * Added support for Python 3.11 and 3.12. * Upgraded scanpy to 1.11 - clustering will be different. -* Added more CCI tests. \ No newline at end of file +* Added more CCI tests. + +.. rubric:: Bugs +* Fixed copy-paste error in louvain.py file. \ No newline at end of file From b613fdb68145507561e529a1d1573dc11aed0597 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 16:38:58 +1000 Subject: [PATCH 179/241] Update to 1.2.1. --- HISTORY.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 09701f34..d9f6e8cc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,6 +8,9 @@ History * Upgraded scanpy to 1.11 - clustering will be different. * Added more CCI tests. +API and Bug Fixes: +* Fixed copy-paste error in louvain.py file. + 1.1.5 (2025-09-17) ------------------ * Add Leiden clustering wrapper. From 9afec1b1d625c9f4da085aa6514617901a705598 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 16:45:23 +1000 Subject: [PATCH 180/241] Missing. --- docs/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 8ea67cc5..3eef636d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,8 @@ undissociated tissue sample. Latest Additions ---------------- +.. include:: release_notes/1.2.1.rst + .. include:: release_notes/1.1.5.rst .. include:: release_notes/1.1.1.rst From c840541e12990fc522ad79653ae51a242968c99b Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 16:46:25 +1000 Subject: [PATCH 181/241] Missing. --- docs/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.rst b/docs/api.rst index c27132ff..45f91e40 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -134,6 +134,7 @@ Tools: `tl` :toctree: api/ tl.clustering.kmeans + tl.clustering.leiden tl.clustering.louvain tl.cci.load_lrs tl.cci.grid From e0b9ec0fc476d0546577e645eea78522d4576fa2 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 17:03:48 +1000 Subject: [PATCH 182/241] Doh. --- docs/release_notes/1.2.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release_notes/1.2.1.rst b/docs/release_notes/1.2.1.rst index 0bbff556..bd91ab0a 100644 --- a/docs/release_notes/1.2.1.rst +++ b/docs/release_notes/1.2.1.rst @@ -1,4 +1,4 @@ -1.1.5 `2025-10-20` +1.2.1 `2025-10-20` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. rubric:: Features From d04675affe5ccc4d40c4120c56325df9639054fb Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 20 Oct 2025 17:05:22 +1000 Subject: [PATCH 183/241] Fix. --- stlearn/tl/clustering/leiden.py | 1 - 1 file changed, 1 deletion(-) diff --git a/stlearn/tl/clustering/leiden.py b/stlearn/tl/clustering/leiden.py index 8a721d3a..b867a420 100644 --- a/stlearn/tl/clustering/leiden.py +++ b/stlearn/tl/clustering/leiden.py @@ -22,7 +22,6 @@ def leiden( ) -> AnnData | None: """\ Wrap function scanpy.tl.leiden - This requires having ran :func:`~scanpy.pp.neighbors` or :func:`~scanpy.external.pp.bbknn` first, or explicitly passing a ``adjacency`` matrix. From 5cae7b48f682286cfe1b9e1a67583a6a81e7a46b Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 21 Oct 2025 07:52:23 +1000 Subject: [PATCH 184/241] Test 3.10 and 3.11 - get rid of patch level. --- .github/workflows/python-package.yml | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index aedd3d86..2c6beaf7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -14,26 +14,27 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [3.12.12] + python-version: [ "3.10", "3.11", "3.12" ] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e .[dev,test] - - name: Check style - run: | - black stlearn tests - ruff check stlearn tests - - name: Check types - run: | - mypy stlearn tests - - name: Test with pytest - run: | - pytest \ No newline at end of file + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[dev,test] + - name: Check style + run: | + black stlearn tests + ruff check stlearn tests + - name: Check types + run: | + mypy stlearn tests + - name: Test with pytest + run: | + pytest \ No newline at end of file From 98a9b919ac4c79a09e236c3c3b7dab34df92d885 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 21 Oct 2025 08:00:59 +1000 Subject: [PATCH 185/241] Missed. --- stlearn/tl/clustering/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stlearn/tl/clustering/__init__.py b/stlearn/tl/clustering/__init__.py index 062244fd..ee183659 100644 --- a/stlearn/tl/clustering/__init__.py +++ b/stlearn/tl/clustering/__init__.py @@ -1,6 +1,7 @@ from .annotate import annotate_interactive from .kmeans import kmeans from .louvain import louvain +from .leiden import leiden __all__ = [ "kmeans", From 8a7a0131562f6fc8da605d88848358e5799b6da6 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 21 Oct 2025 08:02:49 +1000 Subject: [PATCH 186/241] Missed. --- HISTORY.rst | 2 +- docs/index.rst | 2 +- docs/release_notes/{1.2.1.rst => 1.2.2.rst} | 2 +- docs/release_notes/index.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename docs/release_notes/{1.2.1.rst => 1.2.2.rst} (92%) diff --git a/HISTORY.rst b/HISTORY.rst index d9f6e8cc..7d422c70 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,7 +2,7 @@ History ======= -1.2.1 (2025-10-20) +1.2.2 (2025-10-20) ------------------ * Added support for Python 3.11 and 3.12. * Upgraded scanpy to 1.11 - clustering will be different. diff --git a/docs/index.rst b/docs/index.rst index 3eef636d..9e1ac7ae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,7 +38,7 @@ undissociated tissue sample. Latest Additions ---------------- -.. include:: release_notes/1.2.1.rst +.. include:: release_notes/1.2.2.rst .. include:: release_notes/1.1.5.rst diff --git a/docs/release_notes/1.2.1.rst b/docs/release_notes/1.2.2.rst similarity index 92% rename from docs/release_notes/1.2.1.rst rename to docs/release_notes/1.2.2.rst index bd91ab0a..c845d69b 100644 --- a/docs/release_notes/1.2.1.rst +++ b/docs/release_notes/1.2.2.rst @@ -1,4 +1,4 @@ -1.2.1 `2025-10-20` +1.2.2 `2025-10-20` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. rubric:: Features diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 9ed52738..25116f0f 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,7 +1,7 @@ Release Notes =================================================== -.. include:: 1.2.1.rst +.. include:: 1.2.2.rst .. include:: 1.1.5.rst From f867e9289d62391420cd1c6d38fc4e0b6d40de0e Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 21 Oct 2025 08:20:30 +1000 Subject: [PATCH 187/241] Add missing tests and update version. --- pyproject.toml | 2 +- stlearn/tl/clustering/__init__.py | 6 +++--- tests/test_CCI.py | 7 ++++--- tests/test_tools.py | 35 +++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 tests/test_tools.py diff --git a/pyproject.toml b/pyproject.toml index 46850bfe..9837f28b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.2.1" +version = "1.2.2" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] diff --git a/stlearn/tl/clustering/__init__.py b/stlearn/tl/clustering/__init__.py index ee183659..9e5b691f 100644 --- a/stlearn/tl/clustering/__init__.py +++ b/stlearn/tl/clustering/__init__.py @@ -1,11 +1,11 @@ from .annotate import annotate_interactive from .kmeans import kmeans -from .louvain import louvain from .leiden import leiden +from .louvain import louvain __all__ = [ + "annotate_interactive", "kmeans", - "louvain", "leiden", - "annotate_interactive", + "louvain", ] diff --git a/tests/test_CCI.py b/tests/test_CCI.py index 0f9f2cbe..5babfc2a 100644 --- a/tests/test_CCI.py +++ b/tests/test_CCI.py @@ -285,7 +285,7 @@ def test_get_interaction_matrix(self): sig_bool, ligand_bool, receptor_bool, - cell_prop_cutoff=0.2 + cell_prop_cutoff=0.2, ) # Expected: CT1 (A) -> CT2 (D,G): 2 interactions, CT1 -> CT1 (E): 1 interaction @@ -297,8 +297,9 @@ def test_get_interaction_matrix(self): @staticmethod def create_cci(cell_annotations: list[str], unique_cell_type_labels): - cell_data = np.zeros((len(cell_annotations), len(unique_cell_type_labels)), - dtype=np.float64) + cell_data = np.zeros( + (len(cell_annotations), len(unique_cell_type_labels)), dtype=np.float64 + ) for i, annot in enumerate(cell_annotations): ct_index = np.where(unique_cell_type_labels == annot)[0][0] cell_data[i, ct_index] = 1 diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 00000000..1252f5b0 --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +"""Simple tests for clustering tools.""" + +import unittest + +import stlearn as st + +from .utils import read_test_data + + +class TestTools(unittest.TestCase): + + def setUp(self): + self.adata = read_test_data() + st.em.run_pca(self.adata, n_comps=50, random_state=0) + st.pp.neighbors(self.adata, n_neighbors=25, use_rep="X_pca", random_state=0) + + def test_imports(self): + self.assertTrue(callable(st.tl.clustering.annotate_interactive)) + self.assertTrue(callable(st.tl.clustering.kmeans)) + self.assertTrue(callable(st.tl.clustering.leiden)) + self.assertTrue(callable(st.tl.clustering.louvain)) + + def test_kmeans(self): + st.tl.clustering.kmeans(self.adata) + self.assertIn("kmeans", self.adata.obs.columns) + + def test_louvain_runs(self): + st.tl.clustering.louvain(self.adata, resolution=1.0) + self.assertIn("louvain", self.adata.obs.columns) + + def test_leiden_runs(self): + st.tl.clustering.leiden(self.adata, resolution=1.0) + self.assertIn("leiden", self.adata.obs.columns) From dcf114454121b9c954fc5360782d7896c48376ed Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 21 Oct 2025 09:06:59 +1000 Subject: [PATCH 188/241] Fix doc string errors. --- docs/release_notes/1.1.5.rst | 3 +- docs/release_notes/1.2.2.rst | 4 ++- stlearn/embedding/ica.py | 9 +++--- stlearn/embedding/pca.py | 41 ++++++++++++++------------- stlearn/embedding/umap.py | 4 +++ stlearn/preprocessing/filter_genes.py | 4 +++ stlearn/preprocessing/graph.py | 18 ++++++------ stlearn/preprocessing/log_scale.py | 21 ++++++++------ stlearn/preprocessing/normalize.py | 22 +++++++------- stlearn/tl/clustering/leiden.py | 14 +++++---- stlearn/tl/clustering/louvain.py | 19 ++++++++----- 11 files changed, 95 insertions(+), 64 deletions(-) diff --git a/docs/release_notes/1.1.5.rst b/docs/release_notes/1.1.5.rst index 438b088c..eb57927d 100644 --- a/docs/release_notes/1.1.5.rst +++ b/docs/release_notes/1.1.5.rst @@ -2,5 +2,6 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~ .. rubric:: Features + * Add Leiden clustering wrapper. -* Fix documentation, refactor code in spatial.SME. \ No newline at end of file +* Fix documentation, refactor code in spatial.SME. diff --git a/docs/release_notes/1.2.2.rst b/docs/release_notes/1.2.2.rst index c845d69b..fa250503 100644 --- a/docs/release_notes/1.2.2.rst +++ b/docs/release_notes/1.2.2.rst @@ -2,9 +2,11 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~ .. rubric:: Features + * Added support for Python 3.11 and 3.12. * Upgraded scanpy to 1.11 - clustering will be different. * Added more CCI tests. .. rubric:: Bugs -* Fixed copy-paste error in louvain.py file. \ No newline at end of file + +* Fixed copy-paste error in louvain.py file. diff --git a/stlearn/embedding/ica.py b/stlearn/embedding/ica.py index fde64c40..a7b8eade 100644 --- a/stlearn/embedding/ica.py +++ b/stlearn/embedding/ica.py @@ -26,9 +26,10 @@ def run_ica( or 'cube'. You can also provide your own function. It should return a tuple containing the value of the function, and of its derivative, in the - point. Example: - def my_g(x): - return x ** 3, (3 * x ** 2).mean(axis=-1) + point. Example:: + + def my_g(x): + return x ** 3, (3 * x ** 2).mean(axis=-1) tol Tolerance on update at each iteration. use_data @@ -36,11 +37,11 @@ def my_g(x): the chosen data from adata.obsm. copy Return a copy instead of writing to adata. + Returns ------- Depending on `copy`, returns or updates `adata` with the following fields. `X_ica` : :class:`numpy.ndarray` (`adata.obsm`) - Independent Component Analysis representation of data. """ adata = adata.copy() if copy else adata diff --git a/stlearn/embedding/pca.py b/stlearn/embedding/pca.py index 42a2ab54..e6461ca8 100644 --- a/stlearn/embedding/pca.py +++ b/stlearn/embedding/pca.py @@ -20,9 +20,11 @@ def run_pca( ) -> AnnData | None: """\ Wrap function scanpy.pp.pca + Principal component analysis [Pedregosa11]_. Computes PCA coordinates, loadings and variance decomposition. Uses the implementation of *scikit-learn* [Pedregosa11]_. + Parameters ---------- data @@ -38,12 +40,12 @@ def run_pca( Passing `None` decides automatically based on sparseness of the data. svd_solver SVD solver to use: - `'arpack'` (the default - deterministic) - for the ARPACK wrapper in SciPy (:func:`~scipy.sparse.linalg.svds`) - `'randomized'` - for the randomized algorithm due to Halko (2009). - `'auto'` - chooses automatically depending on the size of the problem. + + - `'arpack'` (the default - deterministic) for the ARPACK wrapper in + SciPy (:func:`~scipy.sparse.linalg.svds`) + - `'randomized'` for the randomized algorithm due to Halko (2009). + - `'auto'` chooses automatically depending on the size of the problem. + random_state Change to use different initial states for the optimization. return_info @@ -52,7 +54,7 @@ def run_pca( use_highly_variable Whether to use highly variable genes only, stored in `.var['highly_variable']`. - By default uses them if they have been determined beforehand. + By default, uses them if they have been determined beforehand. dtype Numpy data type string to which to convert the result. copy @@ -65,22 +67,21 @@ def run_pca( chunk_size Number of observations to include in each chunk. Required if `chunked=True` was passed. + Returns ------- - X_pca : :class:`~scipy.sparse.spmatrix`, :class:`~numpy.ndarray` + X_pca: :class:`~scipy.sparse.spmatrix`, :class:`~numpy.ndarray` If `data` is array-like and `return_info=False` was passed, - this function only returns `X_pca`… - adata : anndata.AnnData - …otherwise if `copy=True` it returns or else adds fields to `adata`: - `.obsm['X_pca']` - PCA representation of data. - `.varm['PCs']` - The principal components containing the loadings. - `.uns['pca']['variance_ratio']` - Ratio of explained variance. - `.uns['pca']['variance']` - Explained variance, equivalent to the eigenvalues of the - covariance matrix. + this function only returns `X_pca`. + adata: anndata.AnnData + Otherwise if `copy=True` it returns or else adds fields to `adata`: + + - `.obsm['X_pca']` - PCA representation of data. + - `.varm['PCs']` - The principal components containing the loadings. + - `.uns['pca']['variance_ratio']` - Ratio of explained variance. + - `.uns['pca']['variance']` - Explained variance, equivalent to the + eigenvalues of the covariance matrix. + """ adata = scanpy.pp.pca( diff --git a/stlearn/embedding/umap.py b/stlearn/embedding/umap.py index ad3079ca..e937116c 100644 --- a/stlearn/embedding/umap.py +++ b/stlearn/embedding/umap.py @@ -26,7 +26,9 @@ def run_umap( ) -> AnnData | None: """\ Wrap function scanpy.pp.umap + Embed the neighborhood graph using UMAP [McInnes18]_. + UMAP (Uniform Manifold Approximation and Projection) is a manifold learning technique suitable for visualizing high-dimensional data. Besides tending to be faster than tSNE, it optimizes the embedding such that it best reflects @@ -37,6 +39,7 @@ def run_umap( implementation of `umap-learn `__ [McInnes18]_. For a few comparisons of UMAP with tSNE, see this `preprint `__. + Parameters ---------- adata @@ -48,6 +51,7 @@ def run_umap( If `RandomState`, `random_state` is the random number generator; If `None`, the random number generator is the `RandomState` instance used by `np.random`. + Returns ------- Depending on `copy`, returns or updates `adata` with the following fields. diff --git a/stlearn/preprocessing/filter_genes.py b/stlearn/preprocessing/filter_genes.py index 71bd4b58..876de559 100644 --- a/stlearn/preprocessing/filter_genes.py +++ b/stlearn/preprocessing/filter_genes.py @@ -15,11 +15,14 @@ def filter_genes( Wrap function scanpy.pp.filter_genes Filter genes based on number of cells or counts. + Keep genes that have at least `min_counts` counts or are expressed in at least `min_cells` cells or have at most `max_counts` counts or are expressed in at most `max_cells` cells. + Only provide one of the optional parameters `min_counts`, `min_cells`, `max_counts`, `max_cells` per call. + Parameters ---------- adata @@ -35,6 +38,7 @@ def filter_genes( Maximum number of cells expressed required for a gene to pass filtering. inplace Perform computation inplace or return result. + Returns ------- Depending on `inplace`, returns the following arrays or directly subsets diff --git a/stlearn/preprocessing/graph.py b/stlearn/preprocessing/graph.py index 3f69bfee..4b276aae 100644 --- a/stlearn/preprocessing/graph.py +++ b/stlearn/preprocessing/graph.py @@ -54,11 +54,12 @@ def neighbors( the connectivity of the manifold (`method=='umap'`). If `method=='gauss'`, connectivities are computed according to [Coifman05]_, in the adaption of [Haghverdi16]_. + Parameters ---------- - adata + adata: Annotated data matrix. - n_neighbors + n_neighbors: The size of local neighborhood (in terms of number of neighboring data points) used for manifold approximation. Larger values result in more global views of the manifold, while smaller values result in more local @@ -68,22 +69,23 @@ def neighbors( `n_neighbors` neighbor. {n_pcs} {use_rep} - knn + knn: If `True`, use a hard threshold to restrict the number of neighbors to `n_neighbors`, that is, consider a knn graph. Otherwise, use a Gaussian Kernel to assign low weights to neighbors more distant than the `n_neighbors` nearest neighbor. - random_state + random_state: A numpy random seed. - method + method: Use 'umap' [McInnes18]_ or 'gauss' (Gauss kernel following [Coifman05]_ with adaptive width [Haghverdi16]_) for computing connectivities. - metric + metric: A known metric’s name or a callable that returns a distance. - metric_kwds + metric_kwds: Options for the metric. - copy + copy: Return a copy instead of writing to adata. + Returns ------- Depending on `copy`, updates or returns `adata` with the following: diff --git a/stlearn/preprocessing/log_scale.py b/stlearn/preprocessing/log_scale.py index 4a434507..02320bbb 100644 --- a/stlearn/preprocessing/log_scale.py +++ b/stlearn/preprocessing/log_scale.py @@ -18,21 +18,23 @@ def log1p( Logarithmize the data matrix. Computes :math:`X = \\log(X + 1)`, where :math:`log` denotes the natural logarithm unless a different base is given. + Parameters ---------- - data + data: The (annotated) data matrix of shape `n_obs` × `n_vars`. Rows correspond to cells and columns to genes. - copy + copy: If an :class:`~anndata.AnnData` is passed, determines whether a copy is returned. - chunked + chunked: Process the data matrix in chunks, which will save memory. Applies only to :class:`~anndata.AnnData`. - chunk_size + chunk_size: `n_obs` of the chunks to process the data in. - base + base: Base of the logarithm. Natural logarithm is used by default. + Returns ------- Returns or updates `data`, depending on `copy`. @@ -55,10 +57,12 @@ def scale( Wrap function of scanpy.pp.scale Scale data to unit variance and zero mean. + .. note:: - Variables (genes) that do not display any variation (are constant across - all observations) are retained and set to 0 during this operation. In - the future, they might be set to NaNs. + Variables (genes) that do not display any variation (are constant across + all observations) are retained and set to 0 during this operation. In + the future, they might be set to NaNs. + Parameters ---------- data: @@ -72,6 +76,7 @@ def scale( copy If an :class:`~anndata.AnnData` is passed, determines whether a copy is returned. + Returns ------- Depending on `copy` returns or updates `data` with a scaled `data.X`. diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index e5ecbfad..4bdf2008 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -13,21 +13,22 @@ def normalize_total( inplace: bool = True, ) -> dict[str, np.ndarray] | None: """\ - Wrap function from scanpy.pp.log1p - Normalize counts per cell. + Wrap function from scanpy.pp.log1p - normalize counts per cell. + If choosing `target_sum=1e6`, this is CPM normalization. If `exclude_highly_expressed=True`, very highly expressed genes are excluded from the computation of the normalization factor (size factor) for each cell. This is meaningful as these can strongly influence the resulting normalized values for all other genes [Weinreb17]_. Similar functions are used, for example, by Seurat [Satija15]_, Cell Ranger - [Zheng17]_ or SPRING [Weinreb17]_. - Params - ------ - adata + [Zheng17]_ or SPRING [Weinreb16]_. + + Parameters + ---------- + adata: The annotated data matrix of shape `n_obs` × `n_vars`. Rows correspond to cells and columns to genes. - target_sum + target_sum: If `None`, after normalization, each observation (cell) has a total count equal to the median of total counts for observations (cells) before normalization. @@ -37,16 +38,17 @@ def normalize_total( highly expressed, if it has more than `max_fraction` of the total counts in at least one cell. The not-excluded genes will sum up to `target_sum`. - max_fraction + max_fraction: If `exclude_highly_expressed=True`, consider cells as highly expressed that have more counts than `max_fraction` of the original total counts in at least one cell. - key_added + key_added: Name of the field in `adata.obs` where the normalization factor is stored. - inplace + inplace: Whether to update `adata` or return dictionary with normalized copies of `adata.X` and `adata.layers`. + Returns ------- Returns dictionary with normalized copies of `adata.X` and `adata.layers` diff --git a/stlearn/tl/clustering/leiden.py b/stlearn/tl/clustering/leiden.py index b867a420..fd104719 100644 --- a/stlearn/tl/clustering/leiden.py +++ b/stlearn/tl/clustering/leiden.py @@ -22,9 +22,11 @@ def leiden( ) -> AnnData | None: """\ Wrap function scanpy.tl.leiden + This requires having ran :func:`~scanpy.pp.neighbors` or :func:`~scanpy.external.pp.bbknn` first, or explicitly passing a ``adjacency`` matrix. + Parameters ---------- adata: @@ -58,14 +60,16 @@ def leiden( `obsp` and `neighbors_key` at the same time. copy: Copy adata or modify it inplace. + Returns ------- - :obj:`None` + None or AnnData By default (``copy=False``), updates ``adata`` with the following fields: - ``adata.obs['leiden' | key_added]`` (:class:`pandas.Series`, dtype ``category``) - Array of dim (number of samples) that stores the subgroup id - (``'0'``, ``'1'``, ...) for each cell. - :class:`~anndata.AnnData` + + - ``adata.obs['leiden']`` (:class:`pandas.Series`, dtype ``category``) - + Array of dim (number of samples) that stores the subgroup id + (``'0'``, ``'1'``, ...) for each cell. + When ``copy=True`` is set, a copy of ``adata`` with those fields is returned. """ diff --git a/stlearn/tl/clustering/louvain.py b/stlearn/tl/clustering/louvain.py index 09c7db54..75e386d5 100644 --- a/stlearn/tl/clustering/louvain.py +++ b/stlearn/tl/clustering/louvain.py @@ -16,7 +16,7 @@ def louvain( restrict_to: tuple[str, Sequence[str]] | None = None, key_added: str = "louvain", adjacency: spmatrix | None = None, - flavor: Literal["vtraag", "igraph", "rapids"] = "vtraag", # noqa: F821 + flavor: Literal["vtraag", "igraph", "rapids"] = "vtraag", directed: bool = True, use_weights: bool = False, partition_type: type[MutableVertexPartition] | None = None, @@ -26,13 +26,16 @@ def louvain( ) -> AnnData | None: """\ Wrap function scanpy.tl.louvain + Cluster cells into subgroups [Blondel08]_ [Levine15]_ [Traag17]_. Cluster cells using the Louvain algorithm [Blondel08]_ in the implementation of [Traag17]_. The Louvain algorithm has been proposed for single-cell analysis by [Levine15]_. + This requires having ran :func:`~scanpy.pp.neighbors` or :func:`~scanpy.external.pp.bbknn` first, or explicitly passing a ``adjacency`` matrix. + Parameters ---------- adata: @@ -41,7 +44,7 @@ def louvain( For the default flavor (``'vtraag'``), you can provide a resolution (higher resolution means finding more and smaller clusters), which defaults to 1.0. - See “Time as a resolution parameter” in [Lambiotte09]_. + See "Time as a resolution parameter" in [Lambiotte09]_. random_state: Change the initialization of the optimization. restrict_to: @@ -70,14 +73,16 @@ def louvain( `obsp` and `neighbors_key` at the same time. copy: Copy adata or modify it inplace. + Returns ------- - :obj:`None` + None or AnnData By default (``copy=False``), updates ``adata`` with the following fields: - ``adata.obs['louvain']`` (:class:`pandas.Series`, dtype ``category``) - Array of dim (number of samples) that stores the subgroup id - (``'0'``, ``'1'``, ...) for each cell. - :class:`~anndata.AnnData` + + - ``adata.obs['louvain']`` (:class:`pandas.Series`, dtype ``category``) - + Array of dim (number of samples) that stores the subgroup id + (``'0'``, ``'1'``, ...) for each cell. + When ``copy=True`` is set, a copy of ``adata`` with those fields is returned. """ From d88d5c0980298ece7433cec95d6ce40d54749b32 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 21 Oct 2025 09:09:26 +1000 Subject: [PATCH 189/241] Missed two. --- stlearn/embedding/pca.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stlearn/embedding/pca.py b/stlearn/embedding/pca.py index e6461ca8..915fc1ea 100644 --- a/stlearn/embedding/pca.py +++ b/stlearn/embedding/pca.py @@ -42,7 +42,7 @@ def run_pca( SVD solver to use: - `'arpack'` (the default - deterministic) for the ARPACK wrapper in - SciPy (:func:`~scipy.sparse.linalg.svds`) + SciPy (:func:`~scipy.sparse.linalg.svds`) - `'randomized'` for the randomized algorithm due to Halko (2009). - `'auto'` chooses automatically depending on the size of the problem. @@ -80,7 +80,7 @@ def run_pca( - `.varm['PCs']` - The principal components containing the loadings. - `.uns['pca']['variance_ratio']` - Ratio of explained variance. - `.uns['pca']['variance']` - Explained variance, equivalent to the - eigenvalues of the covariance matrix. + eigenvalues of the covariance matrix. """ From 7cf8b5d45abec1399f06be6ace7e812416593355 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Tue, 21 Oct 2025 09:18:49 +1000 Subject: [PATCH 190/241] Fix SPRING referene. --- docs/references.rst | 6 +----- stlearn/preprocessing/normalize.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/references.rst b/docs/references.rst index a6720aa1..c8323da4 100644 --- a/docs/references.rst +++ b/docs/references.rst @@ -24,7 +24,7 @@ References *UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction*, `arXiv `__. -.. [Weinreb16] Weinreb *et al.* (2016), +.. [Weinreb17] Weinreb *et al.* (2017), *SPRING: a kinetic interface for visualizing high dimensional single-cell expression data*, `bioRxiv `__. @@ -36,10 +36,6 @@ References *Massively parallel digital transcriptional profiling of single cells*, `Nature Communications `__. -.. [Weinreb17] Weinreb *et al.* (2016), - *SPRING: a kinetic interface for visualizing high dimensional single-cell expression data*, - `bioRxiv `__. - .. [Blondel08] Blondel *et al.* (2008), *Fast unfolding of communities in large networks*, `J. Stat. Mech. `__. diff --git a/stlearn/preprocessing/normalize.py b/stlearn/preprocessing/normalize.py index 4bdf2008..d708ccb3 100644 --- a/stlearn/preprocessing/normalize.py +++ b/stlearn/preprocessing/normalize.py @@ -21,7 +21,7 @@ def normalize_total( cell. This is meaningful as these can strongly influence the resulting normalized values for all other genes [Weinreb17]_. Similar functions are used, for example, by Seurat [Satija15]_, Cell Ranger - [Zheng17]_ or SPRING [Weinreb16]_. + [Zheng17]_ or SPRING [Weinreb17]_. Parameters ---------- From b73ac9a34e9744f8867395a56ec8df7c846f6e6d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 24 Oct 2025 12:58:38 +1000 Subject: [PATCH 191/241] Fix number of CPUs. --- stlearn/tl/cci/analysis.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/stlearn/tl/cci/analysis.py b/stlearn/tl/cci/analysis.py index 0905202c..dbe19c1d 100644 --- a/stlearn/tl/cci/analysis.py +++ b/stlearn/tl/cci/analysis.py @@ -5,6 +5,7 @@ import os import numba +import os as os import numpy as np import pandas as pd from anndata import AnnData @@ -77,7 +78,7 @@ def grid( n_row: int = 10, n_col: int = 10, use_label: str | None = None, - n_cpus: int = 1, + n_cpus: int | None = None, verbose: bool = True, ): """Creates a new anndata representing a gridded version of the data; can be @@ -95,7 +96,7 @@ def grid( use_label: str The cell type labels in adata.obs to join together & save as deconvolution data. n_cpus: int - Number of threads to use. + Number of threads to use or if None use os.cpu_count() Returns ------- grid_data: AnnData @@ -108,6 +109,8 @@ def grid( # Setting threads for paralellisation # if n_cpus is not None: numba.set_num_threads(n_cpus) + else: + numba.set_num_threads(os.cpu_count()) # Retrieving the coordinates of each grid # n_squares = n_row * n_col @@ -225,7 +228,7 @@ def run( Number of random pairs of genes to generate when creating the background distribution per LR pair; higher than more accurate p-value estimation. n_cpus: int - The number of cpus to use for multi-threading. + Number of threads to use or if None use os.cpu_count() use_label: str The cell type deconvolution results to use in counting stored in adata.uns; if not specified only considered LR expression without cell @@ -267,8 +270,11 @@ def run( per spot. """ # Setting threads for parallelisation + # Setting threads for paralellisation # if n_cpus is not None: numba.set_num_threads(n_cpus) + else: + numba.set_num_threads(os.cpu_count()) # Making sure none of the var_names contains '_' already, these will need # to be renamed. @@ -526,7 +532,7 @@ def run_cci( cell_prop_cutoff: float = 0.2, p_cutoff: float = 0.05, n_perms: int = 100, - n_cpus: int = 1, + n_cpus: int | None = None, verbose: bool = True, ): """Calls significant celltype-celltype interactions based on cell-type data @@ -561,7 +567,7 @@ def run_cci( raw counting of the cell type interactions with each LR hotspot. This can still be visualised downstream by setting paramters to plot significant interactions to false. - n_cpus: int + n_cpus: int | None cpu resources to use. verbose: bool True if print dialogue to user during run-time. @@ -600,6 +606,8 @@ def run_cci( # Setting threads for paralellisation # if n_cpus is not None: numba.set_num_threads(n_cpus) + else: + numba.set_num_threads(os.cpu_count()) ran_lr = "lr_summary" in adata.uns ran_sig = False if not ran_lr else "n_spots_sig" in adata.uns["lr_summary"].columns From 56d5c7c6e83e37b478ae3edaceae4017ab6d3295 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 25 Oct 2025 19:11:05 +1000 Subject: [PATCH 192/241] Fix --- stlearn/tl/cci/analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stlearn/tl/cci/analysis.py b/stlearn/tl/cci/analysis.py index dbe19c1d..53d5dac7 100644 --- a/stlearn/tl/cci/analysis.py +++ b/stlearn/tl/cci/analysis.py @@ -3,9 +3,9 @@ """ import os +import os as os import numba -import os as os import numpy as np import pandas as pd from anndata import AnnData From a5d11e289a80a503359a7152ebc7e48e9d66c604 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Sat, 25 Oct 2025 21:10:58 +1000 Subject: [PATCH 193/241] Add more tests. --- tests/test_SME.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/test_SME.py b/tests/test_SME.py index 49ea98ec..f031095f 100644 --- a/tests/test_SME.py +++ b/tests/test_SME.py @@ -2,7 +2,9 @@ """Tests for `stlearn` package.""" +import shutil import unittest +from pathlib import Path import scanpy as sc @@ -10,20 +12,27 @@ from .utils import read_test_data -global adata -adata = read_test_data() - class TestSME(unittest.TestCase): """Tests for `stlearn` package.""" - def test_SME(self): - sc.pp.pca(adata) - st.pp.tiling(adata, "./tiling") - st.pp.extract_feature(adata) - import shutil + def setUp(self): + self.adata = read_test_data() + self.tiling_dir = "./tiling" - shutil.rmtree("./tiling") - data_SME = adata.copy() + def tearDown(self): + if Path(self.tiling_dir).exists(): + shutil.rmtree(self.tiling_dir) + + def test_SME(self): + sc.pp.pca(self.adata) + st.pp.tiling(self.adata, self.tiling_dir) + st.pp.extract_feature(self.adata) + self.assertIn("X_tile_feature", self.adata.obsm) + self.assertIn("X_morphology", self.adata.obsm) + self.assertEqual(self.adata.obsm["X_pca"].shape, (316, 50)) + self.assertEqual(self.adata.obsm["X_tile_feature"].shape, (316, 2048)) + self.assertEqual(self.adata.obsm["X_morphology"].shape, (316, 50)) + data_SME = self.adata.copy() # apply stSME to normalise log transformed data st.spatial.SME.SME_normalize(data_SME, use_data="raw") From a6530102ab6a8c1c854ef66198c5ccea27062635 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Thu, 30 Oct 2025 13:51:45 +1000 Subject: [PATCH 194/241] Update HISTORY.rst --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 7d422c70..b6c9b5b3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,7 @@ History * Added more CCI tests. API and Bug Fixes: + * Fixed copy-paste error in louvain.py file. 1.1.5 (2025-09-17) @@ -25,6 +26,7 @@ API and Bug Fixes: * datasets.xenium_sge - loads Xenium data (and caches it) similar to scanpy.visium_sge. API and Bug Fixes: + * Xenium TIFF and cell positions are now aligned. * Consistent with type annotations - mainly missing None annotations. * pl.cluster_plot - Does not keep colours from previous runs when clustering. From 54cf478595cb359ebe5612e9383658be9659f192 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 9 Jan 2026 12:46:59 +1000 Subject: [PATCH 195/241] Update year. --- LICENSE | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index fafffeca..725e1df3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD License -Copyright (c) 2020-2025, Genomics and Machine Learning lab +Copyright (c) 2020-2026, Genomics and Machine Learning lab All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/docs/conf.py b/docs/conf.py index 7ef173a8..dc99d7c8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,7 +34,7 @@ def download_gdrive_file(file_id, filename): # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'stLearn' -copyright = '2022-2025, Genomics and Machine Learning Lab' +copyright = '2022-2026, Genomics and Machine Learning Lab' author = 'Genomics and Machine Learning Lab' html_logo = "images/logo.png" From 63596f7619c7755e3c131ca9cd875520a069a486 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 9 Jan 2026 13:01:42 +1000 Subject: [PATCH 196/241] Fix zip id. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index dc99d7c8..948c580f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ def download_gdrive_file(file_id, filename): # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output def setup(app): if not os.path.isdir("./tutorials"): - download_gdrive_file("1FNMzO4-KsHK8tPd8k5-sTiRS40S97Qs4", "tutorials.zip") + download_gdrive_file("1wiSKmdrP9ZSLu87l_0qiU8eerXxdrubm", "tutorials.zip") os.system("unzip tutorials.zip") return From d93fcfffabff72ee2f89beedd2e7649a59a7da4f Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Mon, 23 Feb 2026 16:47:27 +1000 Subject: [PATCH 197/241] * Fix zip id. * Remove interactive app * Fix some style errors (class names, remove print % usage) * Remove louvain clustering. * Upgrade libraries. * Fix up dependencies. * Upgrade test data. * Remove more multi-case classes/methods/files/imports. * Fixup style of detect_transition_markers.py * Fixup warnings. * Fix methods names to follow lowercase style. --- CONTRIBUTING.rst | 5 +- HISTORY.rst | 9 + docs/api.rst | 26 +- docs/conf.py | 2 +- docs/index.rst | 2 + docs/interactive.rst | 16 - docs/release_notes/1.3.0.rst | 10 + docs/release_notes/index.rst | 2 + pyproject.toml | 56 +- requirements.txt | 15 - stlearn/__init__.py | 24 +- stlearn/adds/add_image.py | 12 +- stlearn/adds/add_mask.py | 36 +- stlearn/adds/annotation.py | 2 +- stlearn/app/__init__.py | 0 stlearn/app/app.py | 481 --- stlearn/app/cli.py | 40 - stlearn/app/requirements.txt | 4 - stlearn/app/source/__init__.py | 0 stlearn/app/source/forms/__init__.py | 0 stlearn/app/source/forms/form_validators.py | 18 - stlearn/app/source/forms/forms.py | 366 -- stlearn/app/source/forms/helper_functions.py | 64 - stlearn/app/source/forms/utils.py | 31 - stlearn/app/source/forms/view_helpers.py | 38 - stlearn/app/source/forms/views.py | 388 -- stlearn/app/source/readme.md | 28 - .../app/static/css/material-dashboard.min.css | 14 - stlearn/app/static/css/style.css | 95 - stlearn/app/static/img/Settings.gif | Bin 35587 -> 0 bytes stlearn/app/static/img/favicon.png | Bin 15406 -> 0 bytes stlearn/app/static/img/loading_gif.gif | Bin 29545 -> 0 bytes .../js/core/bootstrap-material-design.min.js | 1 - stlearn/app/static/js/core/jquery.min.js | 3240 ----------------- stlearn/app/static/js/core/popper.min.js | 834 ----- .../plugins/perfect-scrollbar.jquery.min.js | 2 - stlearn/app/templates/__init__.py | 0 stlearn/app/templates/annotate_plot.html | 20 - stlearn/app/templates/base.html | 213 -- stlearn/app/templates/cci.html | 24 - stlearn/app/templates/cci_old.html | 29 - stlearn/app/templates/cci_plot.html | 20 - stlearn/app/templates/choose_cluster.html | 37 - stlearn/app/templates/cluster_plot.html | 22 - stlearn/app/templates/clustering.html | 46 - stlearn/app/templates/dea.html | 24 - stlearn/app/templates/flash_header.html | 15 - stlearn/app/templates/gene_plot.html | 23 - stlearn/app/templates/index.html | 79 - stlearn/app/templates/lr.html | 24 - stlearn/app/templates/lr_plot.html | 20 - stlearn/app/templates/preprocessing.html | 24 - stlearn/app/templates/progress.html | 19 - stlearn/app/templates/psts.html | 28 - stlearn/app/templates/spatial_cci_plot.html | 20 - stlearn/app/templates/superform.html | 76 - stlearn/app/templates/upload.html | 80 - stlearn/logging.py | 16 +- stlearn/pl/__init__.py | 8 +- stlearn/pl/_docs.py | 2 +- stlearn/pl/cci_plot.py | 4 +- stlearn/pl/cci_plot_helpers.py | 14 +- stlearn/pl/cluster_plot.py | 2 +- stlearn/pl/deconvolution_plot.py | 4 +- stlearn/pl/non_spatial_plot.py | 2 +- stlearn/pl/{QC_plot.py => qc_plot.py} | 2 +- stlearn/pl/subcluster_plot.py | 2 +- stlearn/pl/trajectory/__init__.py | 4 +- stlearn/pl/trajectory/check_trajectory.py | 2 +- ...ansition_plot.py => de_transition_plot.py} | 2 +- stlearn/pl/trajectory/local_plot.py | 2 +- stlearn/pl/trajectory/pseudotime_plot.py | 2 +- stlearn/pl/trajectory/tree_plot.py | 2 +- stlearn/pl/trajectory/tree_plot_simple.py | 2 +- stlearn/pl/trajectory/utils.py | 2 +- stlearn/pl/utils.py | 4 +- stlearn/spatial/SME/__init__.py | 9 - stlearn/spatial/__init__.py | 4 +- stlearn/spatial/clustering/localization.py | 4 +- stlearn/spatial/sme/__init__.py | 9 + .../spatial/{SME => sme}/_weighting_matrix.py | 6 +- stlearn/spatial/{SME => sme}/pseudo_spot.py | 6 +- stlearn/spatial/{SME => sme}/sme_impute0.py | 10 +- stlearn/spatial/{SME => sme}/sme_normalize.py | 8 +- stlearn/spatial/trajectory/__init__.py | 4 +- .../trajectory/detect_transition_markers.py | 83 +- stlearn/spatial/trajectory/global_level.py | 2 +- stlearn/spatial/trajectory/local_level.py | 2 +- stlearn/spatial/trajectory/pseudotime.py | 45 +- stlearn/spatial/trajectory/pseudotimespace.py | 8 +- ..._PAGA.py => shortest_path_spatial_paga.py} | 2 +- .../spatial/trajectory/weight_optimization.py | 4 +- stlearn/tl/cci/analysis.py | 4 +- stlearn/tl/cci/go.py | 2 +- stlearn/tl/cci/het.py | 4 +- stlearn/tl/cci/het_helpers.py | 4 +- stlearn/tl/cci/perm_utils.py | 8 +- stlearn/tl/cci/permutation.py | 6 +- stlearn/tl/clustering/__init__.py | 2 - stlearn/tl/clustering/leiden.py | 23 +- stlearn/tl/clustering/louvain.py | 109 - stlearn/tl/label/__init__.py | 4 +- stlearn/tl/label/label.py | 2 +- stlearn/wrapper/read.py | 12 +- tests/test_PSTS.py | 40 - tests/{test_CCI.py => test_cci.py} | 0 tests/test_cluster_plot.py | 17 +- tests/test_data/test_data.h5 | Bin 185368 -> 193312 bytes tests/test_psts.py | 39 + tests/{test_SME.py => test_sme.py} | 6 +- tests/{test_Spatial.py => test_spatial.py} | 2 +- tests/test_tools.py | 5 - tox.ini | 2 +- 113 files changed, 337 insertions(+), 6942 deletions(-) delete mode 100644 docs/interactive.rst create mode 100644 docs/release_notes/1.3.0.rst delete mode 100644 requirements.txt delete mode 100644 stlearn/app/__init__.py delete mode 100644 stlearn/app/app.py delete mode 100644 stlearn/app/cli.py delete mode 100644 stlearn/app/requirements.txt delete mode 100644 stlearn/app/source/__init__.py delete mode 100644 stlearn/app/source/forms/__init__.py delete mode 100644 stlearn/app/source/forms/form_validators.py delete mode 100644 stlearn/app/source/forms/forms.py delete mode 100644 stlearn/app/source/forms/helper_functions.py delete mode 100644 stlearn/app/source/forms/utils.py delete mode 100644 stlearn/app/source/forms/view_helpers.py delete mode 100644 stlearn/app/source/forms/views.py delete mode 100644 stlearn/app/source/readme.md delete mode 100644 stlearn/app/static/css/material-dashboard.min.css delete mode 100644 stlearn/app/static/css/style.css delete mode 100644 stlearn/app/static/img/Settings.gif delete mode 100644 stlearn/app/static/img/favicon.png delete mode 100644 stlearn/app/static/img/loading_gif.gif delete mode 100644 stlearn/app/static/js/core/bootstrap-material-design.min.js delete mode 100644 stlearn/app/static/js/core/jquery.min.js delete mode 100644 stlearn/app/static/js/core/popper.min.js delete mode 100644 stlearn/app/static/js/plugins/perfect-scrollbar.jquery.min.js delete mode 100644 stlearn/app/templates/__init__.py delete mode 100644 stlearn/app/templates/annotate_plot.html delete mode 100644 stlearn/app/templates/base.html delete mode 100644 stlearn/app/templates/cci.html delete mode 100644 stlearn/app/templates/cci_old.html delete mode 100644 stlearn/app/templates/cci_plot.html delete mode 100644 stlearn/app/templates/choose_cluster.html delete mode 100644 stlearn/app/templates/cluster_plot.html delete mode 100644 stlearn/app/templates/clustering.html delete mode 100644 stlearn/app/templates/dea.html delete mode 100644 stlearn/app/templates/flash_header.html delete mode 100644 stlearn/app/templates/gene_plot.html delete mode 100644 stlearn/app/templates/index.html delete mode 100644 stlearn/app/templates/lr.html delete mode 100644 stlearn/app/templates/lr_plot.html delete mode 100644 stlearn/app/templates/preprocessing.html delete mode 100644 stlearn/app/templates/progress.html delete mode 100644 stlearn/app/templates/psts.html delete mode 100644 stlearn/app/templates/spatial_cci_plot.html delete mode 100644 stlearn/app/templates/superform.html delete mode 100644 stlearn/app/templates/upload.html rename stlearn/pl/{QC_plot.py => qc_plot.py} (99%) rename stlearn/pl/trajectory/{DE_transition_plot.py => de_transition_plot.py} (99%) delete mode 100644 stlearn/spatial/SME/__init__.py create mode 100644 stlearn/spatial/sme/__init__.py rename stlearn/spatial/{SME => sme}/_weighting_matrix.py (98%) rename stlearn/spatial/{SME => sme}/pseudo_spot.py (99%) rename stlearn/spatial/{SME => sme}/sme_impute0.py (94%) rename stlearn/spatial/{SME => sme}/sme_normalize.py (96%) rename stlearn/spatial/trajectory/{shortest_path_spatial_PAGA.py => shortest_path_spatial_paga.py} (98%) delete mode 100644 stlearn/tl/clustering/louvain.py delete mode 100644 tests/test_PSTS.py rename tests/{test_CCI.py => test_cci.py} (100%) create mode 100644 tests/test_psts.py rename tests/{test_SME.py => test_sme.py} (88%) rename tests/{test_Spatial.py => test_spatial.py} (96%) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 720229cb..617b9501 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -72,11 +72,8 @@ Ready to contribute? Here's how to set up `stlearn` for local development. $ cd stlearn/ $ pip install -e .[dev,test] - If you get an error for louvain package on MacOS, make sure you have cmake installed first (if you have brew): - $ brew install cmake - You can also use conda to install these dependencies (after creating the environment): - $ conda install -c conda-forge louvain leidenalg python-igraph + $ conda install -c conda-forge leidenalg python-igraph Or if you prefer pip/virtualenv:: diff --git a/HISTORY.rst b/HISTORY.rst index b6c9b5b3..5dd52cc2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,15 @@ History ======= +1.3.0 (2026-02-23) +------------------ +* Removed interactive stLearn/embedded web application. +* Removed louvain clustering - replaced with leiden. + +API and Bug Fixes: +* Fix import on MutableVertexPartition to use leidenalg.VertexPartition. +* Switch default flavour in leiden to use igraph (and its required parameters). + 1.2.2 (2025-10-20) ------------------ * Added support for Python 3.11 and 3.12. diff --git a/docs/api.rst b/docs/api.rst index 45f91e40..1d3cd5b0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -18,13 +18,14 @@ Wrapper functions: `wrapper` .. autosummary:: :toctree: api/ - Read10X - ReadOldST - ReadSlideSeq - ReadMERFISH - ReadSeqFish - convert_scanpy + read_10x + read_old_st + read_slide_seq + read_merfish + read_seq_fish + read_xenium create_stlearn + convert_scanpy Add: `add` @@ -115,15 +116,15 @@ Spatial: `spatial` spatial.morphology.adjust -.. module:: stlearn.spatial.SME +.. module:: stlearn.spatial.sme .. currentmodule:: stlearn .. autosummary:: :toctree: api/ - spatial.SME.SME_impute0 - spatial.SME.pseudo_spot - spatial.SME.SME_normalize + spatial.sme.sme_impute0 + spatial.sme.pseudo_spot + spatial.sme.sme_normalize Tools: `tl` ------------------- @@ -135,7 +136,6 @@ Tools: `tl` tl.clustering.kmeans tl.clustering.leiden - tl.clustering.louvain tl.cci.load_lrs tl.cci.grid tl.cci.run @@ -151,7 +151,7 @@ Plot: `pl` .. autosummary:: :toctree: api/ - pl.QC_plot + pl.qc_plot pl.gene_plot pl.gene_plot_interactive pl.cluster_plot @@ -183,7 +183,7 @@ Plot: `pl` pl.trajectory.local_plot pl.trajectory.tree_plot pl.trajectory.transition_markers_plot - pl.trajectory.DE_transition_plot + pl.trajectory.de_transition_plot Datasets: `datasets` --------------------------- diff --git a/docs/conf.py b/docs/conf.py index dc99d7c8..948c580f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ def download_gdrive_file(file_id, filename): # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output def setup(app): if not os.path.isdir("./tutorials"): - download_gdrive_file("1FNMzO4-KsHK8tPd8k5-sTiRS40S97Qs4", "tutorials.zip") + download_gdrive_file("1wiSKmdrP9ZSLu87l_0qiU8eerXxdrubm", "tutorials.zip") os.system("unzip tutorials.zip") return diff --git a/docs/index.rst b/docs/index.rst index 9e1ac7ae..659f38cd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,8 @@ undissociated tissue sample. Latest Additions ---------------- +.. include:: release_notes/1.3.0.rst + .. include:: release_notes/1.2.2.rst .. include:: release_notes/1.1.5.rst diff --git a/docs/interactive.rst b/docs/interactive.rst deleted file mode 100644 index 54e85f49..00000000 --- a/docs/interactive.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. highlight:: shell - -=================== -Interactive Web App -=================== - - -Launch stlearn in your local ----------------------------- - -Run the launch command in the terminal: -:: - - stlearn launch - -After that, you can access `https://:5000` in your web browser. diff --git a/docs/release_notes/1.3.0.rst b/docs/release_notes/1.3.0.rst new file mode 100644 index 00000000..e70d11b7 --- /dev/null +++ b/docs/release_notes/1.3.0.rst @@ -0,0 +1,10 @@ +1.3.0 `2026-02-24` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. rubric:: Features + +* Removed interactive stLearn/embedded web application. +* Removed louvain clustering - replaced with leiden. + +.. rubric:: Bugs + diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 25116f0f..819d51ae 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,6 +1,8 @@ Release Notes =================================================== +.. include:: 1.3.0.rst + .. include:: 1.2.2.rst .. include:: 1.1.5.rst diff --git a/pyproject.toml b/pyproject.toml index 9837f28b..ae408643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,26 +4,46 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.2.2" +version = "1.3.0" authors = [ - {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, + { name = "Genomics and Machine Learning Lab", email = "andrew.newman@uq.edu.au" }, ] description = "A downstream analysis toolkit for Spatial Transcriptomic data" -readme = {file = "README.md", content-type = "text/markdown"} -license = {text = "BSD license"} -requires-python = ">=3.10,<3.13" +readme = { file = "README.md", content-type = "text/markdown" } +license = { text = "BSD license" } +requires-python = ">=3.12" +dependencies = [ + "bokeh>=3.7.0,<4.0", + "click>=8.2.0,<9.0", + "igraph>=1.0.0", + "leidenalg>=0.11.0", + "numba>=0.58.1", + "numpy>=1.26.0,<2.0", + "pillow>=11.0.0,<12.0", + "scanpy>=1.11.0,<2.0", + "scikit-image>=0.22.0", + "tensorflow>=2.16", + "keras>=3.0", + "pandas>=2.3.0", + "imageio>=2.37.0,<3.0", + "scipy>=1.11.0,<2.0", + "scikit-learn>=1.7.0,<2.0", +] keywords = ["stlearn"] classifiers = [ "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Framework :: Jupyter", "Intended Audience :: Developers", + "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Natural Language :: English", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Visualization", ] -dynamic = ["dependencies"] - [project.optional-dependencies] dev = [ "black>=23.0", @@ -44,18 +64,12 @@ test = [ "pytest", "pytest-cov", ] -webapp = [ - "flask>=2.0.0", - "flask-wtf>=1.0.0", - "wtforms>=3.0.0", - "markupsafe>2.1.0", -] jupyter = [ "jupyter>=1.0.0", "jupyterlab>=3.0.0", - "ipywidgets>=7.6.0", + "ipywidgets>=8.0.0", "plotly>=5.0.0", - "bokeh>=2.4.0", + "bokeh>=3.7.0,<4.0", "rpy2>=3.4.0", ] @@ -73,15 +87,15 @@ include = ["stlearn", "stlearn.*"] "*" = ["*"] [tool.setuptools.dynamic] -dependencies = {file = ["requirements.txt"]} +dependencies = { file = ["requirements.txt"] } [tool.ruff] -line-length=88 -target-version = "py310" +target-version = "py311" +line-length = 88 [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP"] -ignore = ["E722", "F811", "N802", "N803", "N806", "N818", "N999", "UP031"] +ignore = ["E722", "F811", "N803", "N806", "N818"] exclude = [".git", "__pycache__", "build", "dist"] [tool.ruff.format] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dfe0ccfd..00000000 --- a/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -bokeh>=3.7.0,<4.0 -click>=8.2.0,<9.0 -leidenalg>=0.10.0,<0.11 -louvain>=0.8.2 -numba>=0.58.1 -numpy>=1.26.0,<2.0 -pillow>=11.0.0,<12.0 -scanpy>=1.11.0,<2.0 -scikit-image>=0.22.0,<0.23 -tensorflow>=2.14.1 -keras>=2.14.0 -pandas>=2.3.0 -imageio>=2.37.0,<3.0 -scipy>=1.11.0,<2.0 -scikit-learn>=1.7.0,<2.0 \ No newline at end of file diff --git a/stlearn/__init__.py b/stlearn/__init__.py index 092a2918..aab18147 100644 --- a/stlearn/__init__.py +++ b/stlearn/__init__.py @@ -11,13 +11,13 @@ # Wrapper from .wrapper.read import ( - Read10X, - ReadMERFISH, - ReadOldST, - ReadSeqFish, - ReadSlideSeq, - ReadXenium, create_stlearn, + read_10x, + read_merfish, + read_old_st, + read_seq_fish, + read_slide_seq, + read_xenium, ) # from . import cli @@ -29,12 +29,12 @@ "pl", "spatial", "datasets", - "ReadSlideSeq", - "Read10X", - "ReadOldST", - "ReadMERFISH", - "ReadSeqFish", - "ReadXenium", + "read_slide_seq", + "read_10x", + "read_old_st", + "read_merfish", + "read_seq_fish", + "read_xenium", "create_stlearn", "settings", "types", diff --git a/stlearn/adds/add_image.py b/stlearn/adds/add_image.py index 20376ece..74db3c77 100644 --- a/stlearn/adds/add_image.py +++ b/stlearn/adds/add_image.py @@ -71,15 +71,11 @@ def image( print("Added tissue image to the object!") except: - raise ValueError( - f"""\ + raise ValueError(f"""\ {imgpath!r} does not end on a valid extension. - """ - ) + """) else: - raise ValueError( - f"""\ + raise ValueError(f"""\ {imgpath!r} does not end on a valid extension. - """ - ) + """) return adata if copy else None diff --git a/stlearn/adds/add_mask.py b/stlearn/adds/add_mask.py index d25a488c..8cdfe9e7 100644 --- a/stlearn/adds/add_mask.py +++ b/stlearn/adds/add_mask.py @@ -38,11 +38,9 @@ def add_mask( library_id = list(adata.uns["spatial"].keys())[0] quality = adata.uns["spatial"][library_id]["use_quality"] except: - raise KeyError( - """\ + raise KeyError("""\ Please read ST data first and try again - """ - ) + """) if imgpath is not None and os.path.isfile(imgpath): try: @@ -61,17 +59,13 @@ def add_mask( adata.uns["mask_image"][library_id][key][quality] = img print("Added tissue mask to the object!") except: - raise ValueError( - f"""\ + raise ValueError(f"""\ {imgpath!r} does not end on a valid extension. - """ - ) + """) else: - raise ValueError( - f"""\ + raise ValueError(f"""\ {imgpath!r} does not end on a valid extension. - """ - ) + """) return adata if copy else None @@ -134,11 +128,9 @@ def apply_mask( library_id = list(adata.uns["spatial"].keys())[0] quality = adata.uns["spatial"][library_id]["use_quality"] except: - raise KeyError( - """\ + raise KeyError("""\ Please read ST data first and try again - """ - ) + """) if masks == "all": masks = list(adata.uns["mask_image"][library_id].keys()) @@ -152,22 +144,18 @@ def apply_mask( try: mask_image = adata.uns["mask_image"][library_id][mask][quality] except: - raise KeyError( - f"""\ + raise KeyError(f"""\ Please load mask {mask} images first and try again - """ - ) + """) if select == "black": mask_image = np.where(mask_image > 155, 0, 1) elif select == "white": mask_image = np.where(mask_image > 155, 0, 1) else: - raise ValueError( - """\ + raise ValueError("""\ Only support black and white mask yet. - """ - ) + """) mask_image_2d = mask_image.mean(axis=2) def apply_spot_mask(x): diff --git a/stlearn/adds/annotation.py b/stlearn/adds/annotation.py index 8f5df9db..f7f2854d 100644 --- a/stlearn/adds/annotation.py +++ b/stlearn/adds/annotation.py @@ -4,7 +4,7 @@ def annotation( adata: AnnData, label_list: list[str], - use_label: str = "louvain", + use_label: str = "leiden", copy: bool = False, ) -> AnnData | None: """\ diff --git a/stlearn/app/__init__.py b/stlearn/app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stlearn/app/app.py b/stlearn/app/app.py deleted file mode 100644 index d1e914f3..00000000 --- a/stlearn/app/app.py +++ /dev/null @@ -1,481 +0,0 @@ -import os -import sys -from threading import Thread - -sys.path.append(os.path.dirname(__file__)) - -import asyncio -import tempfile - -import numpy -import numpy as np -import scanpy -from bokeh.application import Application -from bokeh.application.handlers import FunctionHandler -from bokeh.embed import server_document -from bokeh.layouts import row -from bokeh.server.server import Server -from flask import ( - Flask, - flash, - redirect, - render_template, - request, - send_file, - url_for, -) -from tornado.ioloop import IOLoop -from werkzeug.utils import secure_filename - -import stlearn - -# Functions related to processing the forms. -from stlearn.app.source.forms import views # for changing data in response to input - -# Global variables. - -global adata # Storing the data -adata = None -global step_log # Keeps track of what step we're up to (performed preprocessing?) -step_log = { - "uploaded": [False, "Upload file"], - "preprocessed": [False, "Preprocessing"], - "clustering": [False, "Clustering"], - "psts": [False, "Spatial trajectory"], - "dea": [False, "DEA"], - "lr": [False, "Ligand-receptor analysis"], - "cci": [False, "CCI"], - # _params suffix important for templates/progress.html - "preprocessed_params": {}, - "cci_params": {}, - "cluster_params": {}, - "psts_params": {}, - "dea_params": {}, - "lr_params": {}, -} - -# print(stlearn, file=sys.stdout) - -app = Flask(__name__) -app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' - -UPLOAD_FOLDER = tempfile.mkdtemp() -print(UPLOAD_FOLDER) -TEMPLATES_AUTO_RELOAD = True -app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER -app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0 -app.config["TEMPLATES_AUTO_RELOAD"] = TEMPLATES_AUTO_RELOAD -app.config["SESSION_PERMANENT"] = False - - -@app.route("/", methods=["GET"]) -def index(): - return render_template("index.html", step_log=step_log) - - -@app.route("/upload") -def upload(): - return render_template("upload.html", step_log=step_log, flash_bool=True) - - -@app.route("/preprocessing", methods=["GET", "POST"]) -def preprocessing(): - global adata, step_log - updated_page = views.run_preprocessing(request, adata, step_log) - return updated_page - - -@app.route("/clustering", methods=["GET", "POST"]) -def clustering(): - global adata, step_log - updated_page = views.run_clustering(request, adata, step_log) - return updated_page - - -@app.route("/lr", methods=["GET", "POST"]) -def lr(): - global adata, step_log - updated_page = views.run_lr(request, adata, step_log) - return updated_page - - -@app.route("/cci", methods=["GET", "POST"]) -def cci(): - global adata, step_log - updated_page = views.run_cci(request, adata, step_log) - return updated_page - - -@app.route("/psts", methods=["GET", "POST"]) -def psts(): - global adata, step_log - - if "clusters" not in adata.obs.columns: - return redirect(url_for("choose_cluster")) - else: - updated_page = views.run_psts(request, adata, step_log) - return updated_page - - -@app.route("/dea", methods=["GET", "POST"]) -def dea(): - global adata, step_log - updated_page = views.run_dea(request, adata, step_log) - return updated_page - - -allow_files = [ - "filtered_feature_bc_matrix.h5", - "tissue_hires_image.png", - "tissue_lowres_image.png", - "tissue_positions_list.csv", - "scalefactors_json.json", -] - - -@app.route("/folder_uploader", methods=["GET", "POST"]) -def folder_uploader(): - if request.method == "POST": - # Clean uploads folder before upload a new data - import shutil - - shutil.rmtree(app.config["UPLOAD_FOLDER"]) - os.makedirs(app.config["UPLOAD_FOLDER"]) - open(app.config["UPLOAD_FOLDER"] + "/.gitkeep", "a").close() - # os.mknod() - - # Get list of files from selected folder - files = request.files.getlist("file") - - os.mkdir(os.path.join(app.config["UPLOAD_FOLDER"], "spatial")) - - # allow_upload_files = list(map(lambda x: x ),allow_files) - - uploaded = [] - i = 0 - for file in files: - filename = secure_filename(file.filename) - - if allow_files[0] in filename: - file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename)) - os.rename( - os.path.join(app.config["UPLOAD_FOLDER"], filename), - os.path.join(app.config["UPLOAD_FOLDER"], allow_files[0]), - ) - uploaded.append(allow_files[0]) - - for allow_file in allow_files[1:]: - if allow_file in filename: - file.save( - os.path.join(app.config["UPLOAD_FOLDER"] + "/spatial", filename) - ) - os.rename( - os.path.join( - app.config["UPLOAD_FOLDER"] + "/spatial", filename - ), - os.path.join( - app.config["UPLOAD_FOLDER"] + "/spatial", allow_file - ), - ) - - uploaded.append(allow_file) - - print(i) - i += 1 - if len(uploaded) == 5: - flash("File uploaded successfully") - global adata, step_log - # step_log = { - # "uploaded": [False, "Upload file"], - # "preprocessed": [False, "Preprocessing"], - # "clustering": [False, "Clustering"], - # "psts": [False, "Spatial trajectory"], - # "cci_rank": [False, "Cell-cell interaction"], - # "dea": [False, "Differential expression analysis"], - # # _params suffix important for templates/progress.html - # "preprocessed_params": {}, - # "cci_params": {}, - # "cluster_params": {}, - # "psts_params": {}, - # "dea_params": {}, - # } - adata = stlearn.Read10X(app.config["UPLOAD_FOLDER"]) - adata.var_names_make_unique() # removing duplicates - # ensuring compatible format for CCI, since need _ to pair LRs # - adata.var_names = numpy.array( - [var_name.replace("_", "-") for var_name in adata.var_names] - ) - - shutil.rmtree(app.config["UPLOAD_FOLDER"]) - - step_log["uploaded"][0] = True - - return redirect(url_for("upload")) - - if len(uploaded) != 5: - missing_files = [] - for file in allow_files: - if file not in uploaded: - missing_files.append(file) - shutil.rmtree(app.config["UPLOAD_FOLDER"]) - flash("Upload ERROR: Missing " + ", ".join(missing_files)) - return redirect(url_for("upload")) - - -@app.route("/file_uploader", methods=["GET", "POST"]) -def file_uploader(): - if request.method == "POST": - global adata, step_log - - # Clean uploads folder before upload a new data - import shutil - - shutil.rmtree(app.config["UPLOAD_FOLDER"]) - os.makedirs(app.config["UPLOAD_FOLDER"]) - open(app.config["UPLOAD_FOLDER"] + "/.gitkeep", "a").close() - # os.mknod() - f = request.files["file"] - filename = secure_filename(f.filename) - f.save(os.path.join(app.config["UPLOAD_FOLDER"], filename)) - try: - adata = scanpy.read_h5ad(app.config["UPLOAD_FOLDER"] + "/" + f.filename) - except: - flash("Upload ERROR: Please choose the right AnnData file ") - - ### Updating log file with current anndata state ### - step_log["uploaded"][0] = True - - if "n_cells" in adata.var.columns: - step_log["preprocessed"][0] = True - - for col in adata.obs.columns: - if adata.obs[col].dtype.name == "category": - if col != "sub_cluster_labels": - step_log["clustering"][0] = True - - if "global_graph" in adata.uns: - step_log["psts"][0] = True - - step_log["lr"][0] = "lr_summary" in adata.uns - step_log["cci"][0] = np.any(["lr_cci_" in key for key in adata.uns]) - - return redirect(url_for("upload")) - - -@app.route("/choose_cluster", methods=["GET", "POST"]) -def choose_cluster(): - menu = [] - - for col in adata.obs.columns: - if adata.obs[col].dtype.name == "category": - if col != "sub_cluster_labels": - menu.append(col) - - return render_template( - "choose_cluster.html", - template="Flask", - relative_urls=False, - step_log=step_log, - menu=menu, - ) - - -@app.route("/convert_clusters", methods=["GET", "POST"]) -def convert_clusters(): - if request.method == "POST": - adata.obs["clusters"] = adata.obs[request.form["convert_clusters"]] - scanpy.tl.paga(adata, groups="clusters") - stlearn.pl.cluster_plot(adata, use_label="clusters") - - return redirect(url_for("psts")) - - -@app.route("/gene_plot") -def gene_plot(): - script = server_document("http://127.0.0.1:5006/bokeh_gene_plot") - return render_template( - "gene_plot.html", - script=script, - template="Flask", - relative_urls=False, - step_log=step_log, - ) - - -@app.route("/cluster_plot") -def cluster_plot(): - script = server_document("http://127.0.0.1:5006/bokeh_cluster_plot") - return render_template( - "cluster_plot.html", - script=script, - template="Flask", - relative_urls=False, - step_log=step_log, - ) - - -@app.route("/lr_plot") -def lr_plot(): - script = server_document("http://127.0.0.1:5006/bokeh_lr_plot") - return render_template( - "lr_plot.html", - script=script, - template="Flask", - relative_urls=False, - step_log=step_log, - ) - - -@app.route("/spatial_cci_plot") -def spatial_cci_plot(): - script = server_document("http://127.0.0.1:5006/bokeh_spatial_cci_plot") - return render_template( - "spatial_cci_plot.html", - script=script, - template="Flask", - relative_urls=False, - step_log=step_log, - ) - - -@app.route("/annotate_plot") -def annotate_plot(): - script = server_document("http://127.0.0.1:5006/bokeh_annotate_plot") - return render_template( - "annotate_plot.html", - script=script, - template="Flask", - relative_urls=False, - step_log=step_log, - ) - - -@app.route("/save_adata", methods=["POST"]) -def save_adata(): - if request.method == "POST": - fd, path = tempfile.mkstemp() - from datetime import datetime - - now = datetime.now() - date_time = now.strftime("%m-%d-%Y_%H-%M-%S") - - adata.write_h5ad(path) - return send_file( - path, as_attachment=True, attachment_filename="adata_" + date_time + ".h5ad" - ) - - -def modify_doc_gene_plot(doc): - from stlearn.pl.classes_bokeh import BokehGenePlot - - gp_object = BokehGenePlot(adata) - doc.add_root(row(gp_object.layout, width=800)) - - gp_object.data_alpha.on_change("value", gp_object.update_data) - gp_object.tissue_alpha.on_change("value", gp_object.update_data) - gp_object.spot_size.on_change("value", gp_object.update_data) - gp_object.gene_select.on_change("value", gp_object.update_data) - gp_object.cmap_select.on_change("value", gp_object.update_data) - - if len(gp_object.menu) != 0: - gp_object.use_label.on_change("value", gp_object.update_data) - gp_object.output_backend.on_change("value", gp_object.update_data) - - -def modify_doc_cluster_plot(doc): - from stlearn.pl.classes_bokeh import BokehClusterPlot - - gp_object = BokehClusterPlot(adata) - doc.add_root(row(gp_object.layout, width=800)) - - gp_object.use_label.on_change("value", gp_object.update_list) - gp_object.use_label.on_change("value", gp_object.update_data) - gp_object.data_alpha.on_change("value", gp_object.update_data) - gp_object.tissue_alpha.on_change("value", gp_object.update_data) - gp_object.spot_size.on_change("value", gp_object.update_data) - gp_object.list_cluster.on_change("active", gp_object.update_data) - gp_object.checkbox_group.on_change("active", gp_object.update_data) - gp_object.output_backend.on_change("value", gp_object.update_data) - if "rank_genes_groups" in adata.uns: - gp_object.n_top_genes.on_change("value", gp_object.update_data) - gp_object.cmap_select.on_change("value", gp_object.update_data) - gp_object.plot_select.on_change("value", gp_object.update_data) - gp_object.min_logfoldchange.on_change("value", gp_object.update_data) - - -def modify_doc_spatial_cci_plot(doc): - from stlearn.pl.classes_bokeh import BokehSpatialCciPlot - - gp_object = BokehSpatialCciPlot(adata) - doc.add_root(row(gp_object.layout, width=800)) - - gp_object.annot_select.on_change("value", gp_object.update_list) - gp_object.annot_select.on_change("value", gp_object.update_data) - gp_object.lr_select.on_change("value", gp_object.update_data) - gp_object.data_alpha.on_change("value", gp_object.update_data) - gp_object.tissue_alpha.on_change("value", gp_object.update_data) - gp_object.spot_size.on_change("value", gp_object.update_data) - gp_object.list_cluster.on_change("active", gp_object.update_data) - gp_object.output_backend.on_change("value", gp_object.update_data) - - -def modify_doc_lr_plot(doc): - from stlearn.pl.classes_bokeh import BokehLRPlot - - gp_object = BokehLRPlot(adata) - doc.add_root(row(gp_object.layout, width=800)) - - gp_object.data_alpha.on_change("value", gp_object.update_data) - gp_object.tissue_alpha.on_change("value", gp_object.update_data) - gp_object.spot_size.on_change("value", gp_object.update_data) - # gp_object.het_select.on_change("value", gp_object.update_data) - gp_object.lr_select.on_change("value", gp_object.update_data) - gp_object.output_backend.on_change("value", gp_object.update_data) - - -def modify_doc_annotate_plot(doc): - from stlearn.pl.classes_bokeh import Annotate - - gp_object = Annotate(adata) - doc.add_root(row(gp_object.layout, width=800)) - gp_object.data_alpha.on_change("value", gp_object.update_data) - gp_object.tissue_alpha.on_change("value", gp_object.update_data) - gp_object.spot_size.on_change("value", gp_object.update_data) - - -# App for gene_plot -bkapp = Application(FunctionHandler(modify_doc_gene_plot)) - -# App for cluster_plot -bkapp2 = Application(FunctionHandler(modify_doc_cluster_plot)) - -# App for lr_plot -bkapp3 = Application(FunctionHandler(modify_doc_lr_plot)) - -# App for cci_spatial_plot -bkapp3_1 = Application(FunctionHandler(modify_doc_spatial_cci_plot)) - -# App for annotate_plot -bkapp4 = Application(FunctionHandler(modify_doc_annotate_plot)) - - -def bk_worker(): - asyncio.set_event_loop(asyncio.new_event_loop()) - - server = Server( - { - "/bokeh_gene_plot": bkapp, - "/bokeh_cluster_plot": bkapp2, - # "/bokeh_cci_plot": bkapp3, - "/bokeh_lr_plot": bkapp3, - "/bokeh_spatial_cci_plot": bkapp3_1, - "/bokeh_annotate_plot": bkapp4, - }, - io_loop=IOLoop(), - allow_websocket_origin=["127.0.0.1:3000", "localhost:3000"], - ) - server.start() - server.io_loop.start() - - -Thread(target=bk_worker).start() diff --git a/stlearn/app/cli.py b/stlearn/app/cli.py deleted file mode 100644 index 78bfe02b..00000000 --- a/stlearn/app/cli.py +++ /dev/null @@ -1,40 +0,0 @@ -import errno - -import click - -from .. import __version__ - - -@click.group( - name="stlearn", - subcommand_metavar="COMMAND ", - options_metavar="", - context_settings=dict(max_content_width=85, help_option_names=["-h", "--help"]), -) -@click.help_option("--help", "-h", help="Show this message and exit.") -@click.version_option( - version=__version__, - prog_name="stlearn", - message="[%(prog)s] Version %(version)s", - help="Show the software version and exit.", -) -def main(): - click.echo("Please run `stlearn launch` to start the web app") - - -@main.command(short_help="Launch the stlearn interactive app") -def launch(): - from .app import app - - try: - app.run(host="0.0.0.0", port=3000, debug=True, use_reloader=False) - except OSError as e: - if e.errno == errno.EADDRINUSE: - raise click.ClickException( - "Port is in use, please specify an open port using the --port flag." - ) from e - raise - - -if __name__ == "__main__": - main() diff --git a/stlearn/app/requirements.txt b/stlearn/app/requirements.txt deleted file mode 100644 index e6aa1aea..00000000 --- a/stlearn/app/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Flask==2.3.2 -flask_wtf==1.0.0 -markupsafe==2.1.0 -WTForms==3.0.1 diff --git a/stlearn/app/source/__init__.py b/stlearn/app/source/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stlearn/app/source/forms/__init__.py b/stlearn/app/source/forms/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/stlearn/app/source/forms/form_validators.py b/stlearn/app/source/forms/form_validators.py deleted file mode 100644 index 4a279164..00000000 --- a/stlearn/app/source/forms/form_validators.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Contains different kinds of form validators.""" - -from wtforms.validators import ValidationError - - -class CheckNumberRange: - def __init__(self, lower, upper, hint=""): - self.lower = lower - self.upper = upper - self.hint = hint - - def __call__(self, form, field): - if field.data is not None: - if not (self.lower <= float(field.data) <= self.upper): - if self.hint: - raise ValidationError(self.hint) - else: - raise ValidationError("Not in correct range") diff --git a/stlearn/app/source/forms/forms.py b/stlearn/app/source/forms/forms.py deleted file mode 100644 index 466c1da1..00000000 --- a/stlearn/app/source/forms/forms.py +++ /dev/null @@ -1,366 +0,0 @@ -"""Purpose of this script is to create general forms that are programmable with -particular input. Will impliment forms for subsetting the data and -visualisation options in a general way so can be used with any -SingleCellAnalysis dataset. -""" - -import wtforms -from flask_wtf import FlaskForm - -# from flask_wtf.file import FileField -from wtforms import SelectField, SelectMultipleField - - -def createSuperForm(elements, element_fields, element_values, validators=None): - """ Creates a general form; goal is to create a fully programmable form \ - that essentially governs all the options the user will select. - - Args: - elements (list): Element names to be rendered on the page, in \ - order of how they will appear on the page. - - element_fields (list): The names of the fields to be rendered. \ - Each field is in same order as 'elements'. \ - Currently supported are: \ - 'Title', 'SelectMultipleField', 'SelectField', \ - 'StringField', 'Text', 'List'. - - element_values (list): The information which will be put into \ - the field. Changes depending on field: \ - - 'Title' and 'Text': 'object' is a string - containing the title which will be added as \ - a heading when rendered on the page. - - 'SelectMultipleField' and 'SelectField': - 'object' is list of options to select from. - - 'StringField': - The example values to display within the \ - fields text area. The 'placeholder' option. - - 'List': - A list of objects which will be attached \ - to the form. - - validators (list): A list of functions which take the \ - form as input, used to construct the form validator. \ - Form validator constructed by calling these \ - sequentially with form 'self' as input. - - Args: - form (list): A WTForm which has attached as variable all the \ - fields mentioned, so then when rendered as input to - 'SuperDataDisplay.html' shows the form. - """ - - class SuperForm(FlaskForm): - """A base form on which all of the fields will be added.""" - - if validators is None: - validators = [None] * len(elements) - - # Add the information # - SuperForm.elements = elements - SuperForm.element_fields = element_fields - - multiSelectLeft = True # Places multi-select field to left, alternatives - # if many multi-selects in row - for i, element in enumerate(elements): - fieldName = element_fields[i] - - # Adding each element as the appropriate field to the form # - if fieldName == "SelectMultipleField": - setattr( - SuperForm, - element, - SelectMultipleField(element, choices=element_values[i]), - ) - # The point of this number is to give an order for the attributes, - # so that odd numbers get rendered to right of page, even numbers - # left. - setattr(SuperForm, element + "_number", int(multiSelectLeft)) - # inverts, so if left, goes right for the next multiSelectField - multiSelectLeft = not multiSelectLeft - - else: - multiSelectLeft = True # Reset the MultiSelectField position - - if fieldName in ["Title", "List"]: - setattr(SuperForm, element, element_values[i]) - - elif fieldName == "SelectField": - setattr( - SuperForm, - element, - SelectField( - element, choices=element_values[i], validators=validators[i] - ), - ) - - # elif fieldName == 'FileField': - # setattr(SuperForm, element, FileField(validators=validators[i])) - # setattr(SuperForm, element + '_placeholder', # Setting default - # element_values[i]) - - elif fieldName in [ - "StringField", - "IntegerField", - "BooleanField", - "FileField", - "FloatField", - ]: - FieldClass = getattr(wtforms, fieldName) - setattr( - SuperForm, element, FieldClass(element, validators=validators[i]) - ) - setattr( - SuperForm, - element + "_placeholder", # Setting default - element_values[i], - ) - - return SuperForm - - -def getPreprocessForm(): - """Gets the preprocessing form generated from the superform above. - - Returns: - FlaskForm: With attributes that allow for inputs that are related to - pre-processing. - """ - elements = [ - "Spot Quality Control Filtering", # Title - "Minimum genes per spot", - "Minimum counts per spot", - "Gene Quality Control Filtering", # Title - "Minimum spots per gene", - "Minimum counts per gene", - "Normalisation, Log-transform, & Scaling", # Title - "Normalize total", - "Log 1P", - "Scale", - ] - element_fields = [ - "Title", - "IntegerField", - "IntegerField", - "Title", - "IntegerField", - "IntegerField", - "Title", - "BooleanField", - "BooleanField", - "BooleanField", - ] - element_values = ["", 200, 300, "", 3, 5, "", True, True, True] - return createSuperForm(elements, element_fields, element_values) - - -def getLRForm(): - """Gets the LR form generated from the superform above. - - Returns: - FlaskForm: With attributes that allow for inputs that are \ - related to LR analysis. - """ - elements = [ - "Species", - "Spot neighbourhood (-1: smallest neighbourhood, 0: within-spot mode)", - "Minimum spots with LR scores", - "N random gene pairs (permutations)", - "CPUs", - ] - element_fields = [ - "SelectField", - "IntegerField", - "IntegerField", - "IntegerField", - "IntegerField", - ] - element_values = [ - [("Human", "Human"), ("Mouse", "Mouse")], - -1, - 20, - 100, - 2, - ] - return createSuperForm(elements, element_fields, element_values) - - -def getCCIForm(adata): - """Gets the CCI form generated from the superform above. - - Returns: - FlaskForm: With attributes that allow for inputs that are - related to CCI analysis. - """ - elements = [ - "Cell information (only discrete labels available, unless mixture already in " - + "anndata.uns)", - "Minimum spots for LR to be considered", - "Spot mixture (only if the 'Cell Information' label selected available in " - + "anndata.uns)", - "Cell proportion cutoff (value above which cell is considered in spot " - + "if 'Spot mixture' selected)", - "Permutations (recommend atleast 1000)", - ] - element_fields = [ - "SelectField", - "IntegerField", - "BooleanField", - "FloatField", - "IntegerField", - ] - if adata is None: - fields = [] - mix = False - else: - fields = [ - key for key in adata.obs.keys() if isinstance(adata.obs[key].values[0], str) - ] - mix = fields[0] in adata.uns.keys() - element_values = [fields, 20, mix, 0.2, 100] - return createSuperForm(elements, element_fields, element_values) - - -def getCCIForm_old(): - """Gets the CCI form generated from the superform above. - - Returns: - FlaskForm: With attributes that allow for inputs that are related to - CCI analysis. - """ - elements = [ - "* Cell Heterogeneity File", - "Neighbourhood distance (0 indicates within-spot mode)", - "** L-R pair input (e.g. L1_R1, L2_R2, ...)", - "Permutations (0 indicates no permutation testing)", - ] - element_fields = ["FileField", "IntegerField", "StringField", "IntegerField"] - element_values = ["", 25, "", 0] - return createSuperForm(elements, element_fields, element_values) - - -def getClusterForm(): - """Gets the Cluster form generated using superform above. - - Returns: - FlaskForm: With attributes that allow input related to clustering. - """ - elements = [ - "PCA components", - "stSME normalisation", - "Cluster method", - "K", - "Resolution", - "Neighbours (for Louvain/Leiden)", - ] - element_fields = [ - "IntegerField", - "BooleanField", - "SelectField", - "IntegerField", - "FloatField", - "IntegerField", - ] - element_values = [ - 50, - True, - [("KMeans", "KMeans"), ("Louvain", "Louvain"), ("Leiden", "Leiden")], - 10, - 1.0, - 15, - ] - return createSuperForm(elements, element_fields, element_values) - - -def getPSTSForm(trajectory, clusts, options): - """Gets the psts form generated using superform above. - - Args: - cluster_set (numpy.array): The clusters which can be selected as - the root for psts analysis. - - Returns: - FlaskForm: With attributes that allow input related to psts. - """ - elements = [ - "Root cluster", - "Reverse", - "eps (max. dist. spot neighbourhood)", - "Trajectory Select", - "Select distance-based method", - ] - element_fields = [ - "SelectField", - "BooleanField", - "IntegerField", - "SelectField", - "SelectField", - ] - - element_values = [clusts, False, 50, trajectory, options] - return createSuperForm(elements, element_fields, element_values) - - -def getDEAForm(list_labels, methods): - """Gets the psts form generated using superform above. - - Args: - cluster_set (numpy.array): The clusters which can be selected as - the root for psts analysis. - - Returns: - FlaskForm: With attributes that allow input related to psts. - """ - elements = ["Use label", "Use method"] - element_fields = [ - "SelectField", - "SelectField", - ] - - element_values = [list_labels, methods] - return createSuperForm(elements, element_fields, element_values) - - -######################## Junk Code ############################################# -# def getCCIForm(step_log): -# """ Gets the CCI form generated from the superform above. -# -# Returns: -# FlaskForm: With attributes that allow for inputs that are related to -# CCI analysis. -# """ -# elements, element_fields, element_values = [], [], [] -# if type(step_log['cci_het']) == type(None): -# # Analysis type form version # -# analysis_elements = ['Cell Heterogeneity Information', # Title -# 'cci_het', -# 'Permutation Testing', # Title -# 'cci_perm'] -# analysis_fields = ['Title', 'SelectField', 'Title', 'SelectField'] -# label_transfer_options = ['Upload Cell Label Transfer', -# 'No Cell Label Transfer'] -# permutation_options = ['With permutation testing', -# 'Without permutation testing'] -# analysis_values = ['', label_transfer_options, '', permutation_options] -# elements += analysis_elements -# element_fields += analysis_fields -# element_values += analysis_values -# -# else: -# # Core elements regardless of CCI mode # -# elements += ['Neighbourhood distance', -# 'L-R pair input (e.g. L1_R1, L2_R2, ...)'] -# element_fields += ['IntegerField', 'StringField'] -# element_values += [5, ''] -# -# if step_log['cci_perm']: -# # Including cell heterogeneity information # -# elements += ['Permutations'] -# element_fields += ['IntegerField'] -# element_values += [200] -# -# return createSuperForm(elements, element_fields, element_values, None) diff --git a/stlearn/app/source/forms/helper_functions.py b/stlearn/app/source/forms/helper_functions.py deleted file mode 100644 index 692c98a9..00000000 --- a/stlearn/app/source/forms/helper_functions.py +++ /dev/null @@ -1,64 +0,0 @@ -# Purpose of this script is to write the functions that help facilitate -# subsetting of the data depending on the users input - - -def printOut(text, fileName="stdout.txt", close=True, file=None): - """Prints to the specified file name. Used for debugging. - If close is Fale, returns open file. - """ - - if file is None: - file = open(fileName, "w") - - print(text, file=file) - - if close: - file.close() - else: - return file - - -def filterOptions(metaDataSets, options): - """Returns options that overlap with keys in metaDataSets dictionary""" - if options is None: - options = list(metaDataSets.keys()) - else: - options = [option for option in options if option in metaDataSets.keys()] - - return options - - -def addChoices(metaDataSets, options, elementValues): - """Helper function which generates choices for SelectMultiField""" - for option in options: - choices = [(optioni, optioni) for optioni in metaDataSets[option]] - elementValues.append(choices) - - -# TODO update this so has 'options' as input -def subsetSCA(sca, subsetForm): - """Subsets the SCA based on the selected fields and the inputted genes.""" - - # Getting the attached fields from the form which refer subset options # - options = filterOptions(sca.metaDataSets, subsetForm.elements) - - # Subsetting based on selection # - conditionSelection = {} # selection dictionary - for i, option in enumerate(options): - selected = getattr(subsetForm, option).data - if len(selected) != 0: - conditionSelection[option] = selected - - # Subsetting based on conditions # - if len(conditionSelection) != 0: - sca = sca.createConditionSubset("subset", conditionSelection) - - # Subsetting based on inputted genes # - geneList = getattr(subsetForm, "Select Cells Expressing Gene/s").data.split(",") - if geneList != [""]: - # Filter to just the genes which express all of the inputted genes # - sca = sca.createGeneExprsSubset( - "subset", genesToFilter=geneList, cutoff=0, keep=True, useOr=False - ) - - return sca, conditionSelection, geneList diff --git a/stlearn/app/source/forms/utils.py b/stlearn/app/source/forms/utils.py deleted file mode 100644 index 42121bcf..00000000 --- a/stlearn/app/source/forms/utils.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Helper utilities and decorators.""" - -from flask import flash - - -def flash_errors(form, category="warning"): - """Flash all errors for a form.""" - for field, errors in form.errors.items(): - for error in errors: - flash(getattr(form, field).label.text + " - " + error + ", category") - - -def get_all_paths(adata): - import networkx as nx - - G = nx.from_numpy_array(adata.uns["paga"]["connectivities_tree"].toarray()) - mapping = {int(k): v for k, v in zip(G.nodes, adata.obs.clusters.cat.categories)} - G = nx.relabel_nodes(G, mapping) - - all_paths = [] - for source in G.nodes: - for target in G.nodes: - paths = nx.all_simple_paths(G, source=source, target=target) - for path in paths: - all_paths.append(path) - - import numpy as np - - all_paths = list(map(lambda x: " - ".join(np.array(x).astype(str)), all_paths)) - - return all_paths diff --git a/stlearn/app/source/forms/view_helpers.py b/stlearn/app/source/forms/view_helpers.py deleted file mode 100644 index 3c2de3d0..00000000 --- a/stlearn/app/source/forms/view_helpers.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Helper functions for views.py.""" - - -def getVal(form, element): - return getattr(form, element).data - - -def getData(form): - """Retrieves the data from the form and places into dictionary.""" - params = {} - form_elements = form.elements - form_fields = form.element_fields - for i, element in enumerate(form_elements): - if form_fields[i] != "Title": - data = getVal(form, element) - params[element] = data - return params - - -def getLR(lr_input, gene_names): - """Returns list of lr_inputs and error message, if any.""" - if lr_input == "": - return None, "ERROR: LR pairs required input." - - try: - lrs = [lr.strip(" ") for lr in lr_input.split(",")] - absent_genes = [] - for lr in lrs: - genes = lr.split("_") - absent_genes.extend([gene for gene in genes if gene not in gene_names]) - - if len(absent_genes) != 0: - return None, f"ERROR: inputted genes not found {absent_genes}." - - return lrs, "" - - except: - return None, "ERROR: LR pairs misformatted." diff --git a/stlearn/app/source/forms/views.py b/stlearn/app/source/forms/views.py deleted file mode 100644 index 3a857319..00000000 --- a/stlearn/app/source/forms/views.py +++ /dev/null @@ -1,388 +0,0 @@ -""" This is more a general views focussed on defining functions which are \ - called by other views for specify pages. This way different pages can be \ - used to display different data, but in a consistent way. -""" - -import sys -import traceback - -import numpy -import numpy as np -import scanpy as sc -from flask import flash, render_template - -import stlearn as st -import stlearn.app.source.forms.view_helpers as vhs -from stlearn.app.source.forms import forms -from stlearn.app.source.forms.utils import flash_errors - -# Creating the forms using a class generator # -PreprocessForm = forms.getPreprocessForm() -# CCIForm = forms.getCCIForm() #OLD -ClusterForm = forms.getClusterForm() -LRForm = forms.getLRForm() - - -def run_preprocessing(request, adata, step_log): - """Performs the scanpy pre-processing steps based on the inputted data.""" - - form = PreprocessForm(request.form) - - if not form.validate_on_submit(): - flash_errors(form) - - elif adata is None: - flash("Need to load data first!") - - else: - # Logging params used # - step_log["preprocessed_params"] = vhs.getData(form) - print(step_log["preprocessed_params"], file=sys.stdout) - - # QC filtering # - sc.pp.filter_cells(adata, min_genes=vhs.getVal(form, "Minimum genes per spot")) - sc.pp.filter_cells( - adata, min_counts=vhs.getVal(form, "Minimum counts per spot") - ) - sc.pp.filter_genes(adata, min_cells=vhs.getVal(form, "Minimum spots per gene")) - sc.pp.filter_genes( - adata, min_counts=vhs.getVal(form, "Minimum counts per gene") - ) - - # Pre-processing # - if vhs.getVal(form, "Normalize total"): - sc.pp.normalize_total(adata, target_sum=1e4) - if vhs.getVal(form, "Log 1P"): - sc.pp.log1p(adata) - adata.raw = adata - if vhs.getVal(form, "Scale"): - sc.pp.scale(adata, max_value=10) - - # Setting pre-process to true # - step_log["preprocessed"][0] = True - - if step_log["preprocessed"][0]: - flash("Preprocessing is completed!") - - updated_page = render_template( - "preprocessing.html", - title=step_log["preprocessed"][1], - preprocess_form=form, - flash_bool=step_log["preprocessed"][0], - step_log=step_log, - ) - - return updated_page - - -def run_lr(request, adata, step_log): - """Runs LR analysis.""" - - form = LRForm(request.form) - - if not form.validate_on_submit(): - flash_errors(form) - - elif adata is None: - flash("Need to load data first!") - - else: - step_log["lr_params"] = vhs.getData(form) - print(step_log["lr_params"], file=sys.stdout) - # order: Species, Spot neighbourhood, min_spots, n_pairs, CPUs - element_values = list(step_log["lr_params"].values()) - dist = element_values[1] - dist = dist if dist != -1 else None - - # Loading the LR databases available within stlearn (from NATMI) - lrs = st.tl.cci.load_lrs(["connectomeDB2020_lit"], species=element_values[0]) - - # Running the analysis # - st.tl.cci.run( - adata, - lrs, - min_spots=element_values[2], - distance=dist, - n_pairs=element_values[3], - n_cpus=element_values[-1], - ) - flash("LR analysis is completed!") - - step_log["lr"][0] = "lr_summary" in adata.uns - - updated_page = render_template( - "lr.html", - title=step_log["lr"][1], - lr_form=form, - flash_bool=True, - step_log=step_log, - ) - return updated_page - - -def run_cci(request, adata, step_log): - """Performs CCI analysis.""" - - CCIForm = forms.getCCIForm(adata) - form = CCIForm(request.form) - - if not form.validate_on_submit(): - flash_errors(form) - - elif adata is None: - flash("Need to load data first!") - - else: - step_log["cci_params"] = vhs.getData(form) - print(step_log["cci_params"], file=sys.stdout) - # order: cell_type, min_spots, spot_mixtures, cell_prop_cutoff, sig_spots - # n_perms - element_values = list(step_log["cci_params"].values()) - - if not form.validate_on_submit(): - flash_errors(form) - - else: - try: - # Running the counting of co-occurence of cell types and LR expression # - st.tl.cci.run_cci( - adata, - element_values[0], - min_spots=element_values[1], - spot_mixtures=element_values[2], - cell_prop_cutoff=element_values[3], - sig_spots=True, # Should make this not optional.. - n_perms=element_values[4], - ) - - flash("CCI analysis is completed!") - - except Exception as msg: - traceback.print_exc(file=sys.stdout) - flash("Analysis ERROR: " + str(msg)) - print(msg) - - step_log["cci"][0] = np.any(["lr_cci_" in key for key in adata.uns]) - - updated_page = render_template( - "cci.html", - title=step_log["cci"][1], - cci_form=form, - flash_bool=True, - step_log=step_log, - ) - - return updated_page - - -def run_clustering(request, adata, step_log): - """Performs clustering analysis.""" - - form = ClusterForm(request.form) - - step_log["cluster_params"] = vhs.getData(form) - print(step_log["cluster_params"], file=sys.stdout) - # order: pca_comps, SME bool, method, method_param - element_values = list(step_log["cluster_params"].values()) - - if not form.validate_on_submit(): - flash_errors(form) - - elif adata is None: - flash("Need to load data first!") - - else: - try: - # Running PCA, performs scaling internally # - n_comps = element_values[0] - st.em.run_pca(adata, n_comps=n_comps) - - print(element_values[1], file=sys.stdout, flush=True) - if element_values[1]: # Performing SME clustering # - # Image feature extraction # - st.pp.tiling(adata) - st.pp.extract_feature(adata) - - # apply stSME to data (format of data depending on preprocess) - st.spatial.SME.SME_normalize(adata, use_data="raw") - adata.X = adata.obsm["raw_SME_normalized"] - st.em.run_pca(adata, n_comps=n_comps) - - # Performing the clustering on the PCA # - if element_values[2] == "KMeans": # KMeans - param = int(element_values[3]) - st.tl.clustering.kmeans(adata, n_clusters=param, use_data="X_pca") - - st.pp.neighbors(adata, n_neighbors=element_values[5], use_rep="X_pca") - sc.tl.paga(adata, groups="kmeans") - st.pl.cluster_plot(adata, use_label="kmeans") - - elif element_values[2] == "Louvain": # Louvain - param = element_values[4] - st.pp.neighbors(adata, n_neighbors=element_values[5], use_rep="X_pca") - st.tl.clustering.louvain(adata, resolution=param) - sc.tl.paga(adata, groups="louvain") - st.pl.cluster_plot(adata, use_label="louvain") - - else: # Leiden - param = element_values[4] - st.pp.neighbors(adata, n_neighbors=element_values[5], use_rep="X_pca") - sc.tl.leiden(adata, resolution=param) - sc.tl.paga(adata, groups="leiden") - st.pl.cluster_plot(adata, use_label="leiden") - - step_log["clustering"][0] = True - flash("Clustering is completed!") - - except Exception as msg: - traceback.print_exc(file=sys.stdout) - flash("Analysis ERROR: " + str(msg)) - print(msg) - - updated_page = render_template( - "clustering.html", - title=step_log["clustering"][1], - clustering_form=form, - flash_bool=True, - step_log=step_log, - ) - - return updated_page - - -def run_psts(request, adata, step_log): - """Performs psts analysis; must have performed clustering first.""" - # Creating the form with the clustering information # - cluster_set = numpy.unique(adata.obs["clusters"].values) - order = numpy.argsort([int(cluster) for cluster in cluster_set]) - cluster_set = cluster_set[order] - - options = ["Auto", "Spatial distance only", "Gene expression distance only"] - - from .utils import get_all_paths - - trajectory_set = get_all_paths(adata) - - PSTSForm = forms.getPSTSForm(trajectory_set, cluster_set, options) - form = PSTSForm(request.form) - - step_log["psts_params"] = vhs.getData(form) - print(step_log["psts_params"], file=sys.stdout) - # order: pca_comps, SME bool, method, method_param - element_values = list(step_log["psts_params"].values()) - - if element_values[4] == "Auto": - model = "mixed" - elif element_values[4] == "Spatial distance only": - model = "spatial" - else: - model = "gene_expression" - - if not form.validate_on_submit(): - flash_errors(form) - - elif adata is None: - flash("Need to load data first!") - - else: - try: - from stlearn.spatial.trajectory import set_root - - root_index = set_root( - adata, use_label="clusters", cluster=str(element_values[0]) - ) - - adata.uns["iroot"] = root_index - - print(root_index, file=sys.stdout, flush=True) - - # Performing the TI # - print(element_values[3], file=sys.stdout, flush=True) - - node_order = element_values[3].split(" - ") - - st.spatial.trajectory.pseudotime( - adata, - eps=element_values[2], - use_rep="X_pca", - use_label="clusters", - reverse=element_values[1], - ) - print(node_order) - st.spatial.trajectory.pseudotimespace_global( - adata, use_label="clusters", list_clusters=node_order, model=model - ) - - st.pl.cluster_plot( - adata, - use_label="clusters", - show_trajectories=True, - list_clusters=node_order, - show_subcluster=True, - ) - - step_log["psts"][0] = True - flash("Trajectory inference is completed!") - - except Exception as msg: - traceback.print_exc(file=sys.stdout) - flash("Analysis ERROR: " + str(msg)) - print(msg) - - updated_page = render_template( - "psts.html", - title=step_log["psts"][1], - psts_form=form, - flash_bool=True, - step_log=step_log, - ) - - return updated_page - - -def run_dea(request, adata, step_log): - list_labels = [] - - for col in adata.obs.columns: - if adata.obs[col].dtype.name == "category": - if col != "sub_cluster_labels": - list_labels.append(col) - - list_labels = numpy.array(list_labels) - - methods = numpy.array(["t-test", "t-test_overestim_var", "logreg", "wilcoxon"]) - - DEAForm = forms.getDEAForm(list_labels, methods) - form = DEAForm(request.form) - - step_log["dea_params"] = vhs.getData(form) - print(step_log["dea_params"], file=sys.stdout) - element_values = list(step_log["dea_params"].values()) - - if not form.validate_on_submit(): - flash_errors(form) - - elif adata is None: - flash("Need to load data first!") - - else: - try: - sc.tl.rank_genes_groups(adata, element_values[0], method=element_values[1]) - - step_log["dea"][0] = True - flash("Differential expression analysis is completed!") - - except Exception as msg: - traceback.print_exc(file=sys.stdout) - flash("Analysis ERROR: " + str(msg)) - print(msg) - - updated_page = render_template( - "dea.html", - title=step_log["dea"][1], - dea_form=form, - flash_bool=True, - step_log=step_log, - ) - - return updated_page diff --git a/stlearn/app/source/readme.md b/stlearn/app/source/readme.md deleted file mode 100644 index 1ff1c27c..00000000 --- a/stlearn/app/source/readme.md +++ /dev/null @@ -1,28 +0,0 @@ -# Pre-processing Form Design Notes -Flow of data: - - app.py/preprocessing() - -> source/forms/views.py/run_preprocessing(request, adata, step_log) - -> source/forms/forms.py/getPreprocessForm() - -> templates/preprocessing.html - -> templates/superform.html - -Notes: - - * step_log defined in app.py, keeps track whether pre-processing was run. - -> If not run, then run_preprocessing shows just the form. - -> If has run, shows banner that preprocessing complete. - - * If attempt to run_preprocessing() when adata not yet loaded, shows banner - indicating need to upload the data first. - - * source/forms/forms.py/getPreprocessForm() generates a WTForm class using a - general WTForm generator, as defined in: - source/forms/forms.py/createSuperForm() - - * templates/preprocessing.html is the preprocessing page, which also injects - in a form to display using templates/superform.html. - - * templates/superform.html renders a general WTForm that was created using - the source/forms/forms.py/createSuperForm() function, thereby allowing - easy generation of new forms if need to add extra information. diff --git a/stlearn/app/static/css/material-dashboard.min.css b/stlearn/app/static/css/material-dashboard.min.css deleted file mode 100644 index 11537572..00000000 --- a/stlearn/app/static/css/material-dashboard.min.css +++ /dev/null @@ -1,14 +0,0 @@ -/*! - - ========================================================= - * Material Dashboard - v2.1.0 - ========================================================= - - * Product Page: https://www.creative-tim.com/product/material-dashboard - * Copyright 2020 Creative Tim (http://www.creative-tim.com) - - ========================================================= - - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - */.card{font-size:.875rem}@media print{*,:after,:before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]:after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}.container,body{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}*,:after,:before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:Roboto,Helvetica,Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fafafa}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;text-decoration:underline dotted;cursor:help;border-bottom:0}address{font-style:normal;line-height:inherit}address,dl,ol,ul{margin-bottom:1rem}dl,ol,ul{margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0a6ebd;text-decoration:underline}a:not([href]):not([tabindex]),a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:400;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:7rem}.display-1,.display-2{font-weight:300;line-height:1.2}.display-2{font-size:3.5rem}.display-3{font-size:2.8125rem}.display-3,.display-4{font-weight:300;line-height:1.2}.display-4{font-size:2.125rem}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer:before{content:"\2014 \00A0"}.img-fluid,.img-thumbnail{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fafafa;border:1px solid #dee2e6;border-radius:.25rem;box-shadow:0 1px 2px rgba(0,0,0,.075)}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}code{font-size:87.5%;color:#e91e63;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem;box-shadow:inset 0 -.1rem 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:500;box-shadow:none}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col-auto,.col-lg,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-auto,.col-md,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md-auto,.col-sm,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{flex-basis:0;flex-grow:1;max-width:100%}.col-auto{flex:0 0 auto;width:auto;max-width:none}.col-1{flex:0 0 8.333333%;max-width:8.333333%}.col-2{flex:0 0 16.666667%;max-width:16.666667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.333333%;max-width:33.333333%}.col-5{flex:0 0 41.666667%;max-width:41.666667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.333333%;max-width:58.333333%}.col-8{flex:0 0 66.666667%;max-width:66.666667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.333333%;max-width:83.333333%}.col-11{flex:0 0 91.666667%;max-width:91.666667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{flex-basis:0;flex-grow:1;max-width:100%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:none}.col-sm-1{flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{flex-basis:0;flex-grow:1;max-width:100%}.col-md-auto{flex:0 0 auto;width:auto;max-width:none}.col-md-1{flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{flex-basis:0;flex-grow:1;max-width:100%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:none}.col-lg-1{flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{flex-basis:0;flex-grow:1;max-width:100%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:none}.col-xl-1{flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid rgba(0,0,0,.06)}.table thead th{vertical-align:bottom;border-bottom:2px solid rgba(0,0,0,.06)}.table tbody+tbody{border-top:2px solid rgba(0,0,0,.06)}.table .table{background-color:#fafafa}.table-sm td,.table-sm th{padding:.3rem}.table-bordered,.table-bordered td,.table-bordered th{border:1px solid rgba(0,0,0,.06)}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#c1e2fc}.table-hover .table-primary:hover,.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#a9d7fb}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#cde9ce}.table-hover .table-success:hover,.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#bbe1bd}.table-info,.table-info>td,.table-info>th{background-color:#b8ecf3}.table-hover .table-info:hover,.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#a2e6ef}.table-warning,.table-warning>td,.table-warning>th{background-color:#fff9c8}.table-hover .table-warning:hover,.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fff6af}.table-danger,.table-danger>td,.table-danger>th{background-color:#fccac7}.table-hover .table-danger:hover,.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#fbb3af}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover,.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover,.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th,.table-hover .table-active:hover,.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fafafa;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:rgba(0,0,0,.06)}.table-dark{color:#fafafa;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:hsla(0,0%,100%,.05)}.table-dark.table-hover tbody tr:hover{background-color:hsla(0,0%,100%,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;padding:.4375rem 0;font-size:1rem;line-height:1.5;color:#495057;background-color:transparent;background-clip:padding-box;border:1px solid #d2d2d2;box-shadow:none;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:transparent;border-color:#9acffa;outline:0;box-shadow:none,0 0 0 .2rem rgba(33,150,243,.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:not([size]):not([multiple]){height:calc(2.4375rem + 2px)}select.form-control:focus::-ms-value{color:#495057;background-color:transparent}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.4375rem + 1px);padding-bottom:calc(.4375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5625rem + 1px);padding-bottom:calc(.5625rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.4375rem;padding-bottom:.4375rem;margin-bottom:0;line-height:1.5;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm,.input-group-lg>.form-control-plaintext.form-control,.input-group-lg>.input-group-append>.form-control-plaintext.btn,.input-group-lg>.input-group-append>.form-control-plaintext.input-group-text,.input-group-lg>.input-group-prepend>.form-control-plaintext.btn,.input-group-lg>.input-group-prepend>.form-control-plaintext.input-group-text,.input-group-sm>.form-control-plaintext.form-control,.input-group-sm>.input-group-append>.form-control-plaintext.btn,.input-group-sm>.input-group-append>.form-control-plaintext.input-group-text,.input-group-sm>.input-group-prepend>.form-control-plaintext.btn,.input-group-sm>.input-group-prepend>.form-control-plaintext.input-group-text{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem 0;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-sm>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:calc(2.125rem + 2px)}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5625rem 0;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-lg>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:calc(4.125rem + 2px)}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#4caf50}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(76,175,80,.8);border-radius:.2rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#4caf50}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#4caf50;box-shadow:0 0 0 .2rem rgba(76,175,80,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#4caf50}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#4caf50}.custom-control-input.is-valid~.custom-control-label:before,.was-validated .custom-control-input:valid~.custom-control-label:before{background-color:#a3d7a5}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .custom-control-input:valid:checked~.custom-control-label:before{background-color:#6ec071}.custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem rgba(76,175,80,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#4caf50}.custom-file-input.is-valid~.custom-file-label:before,.was-validated .custom-file-input:valid~.custom-file-label:before{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(76,175,80,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#f44336}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(244,67,54,.8);border-radius:.2rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#f44336}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#f44336;box-shadow:0 0 0 .2rem rgba(244,67,54,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#f44336}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#f44336}.custom-control-input.is-invalid~.custom-control-label:before,.was-validated .custom-control-input:invalid~.custom-control-label:before{background-color:#fbb4af}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label:before,.was-validated .custom-control-input:invalid:checked~.custom-control-label:before{background-color:#f77066}.custom-control-input.is-invalid:focus~.custom-control-label:before,.was-validated .custom-control-input:invalid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem rgba(244,67,54,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#f44336}.custom-file-input.is-invalid~.custom-file-label:before,.was-validated .custom-file-input:invalid~.custom-file-label:before{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(244,67,54,.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{justify-content:center}.form-inline .form-group,.form-inline label{display:flex;align-items:center;margin-bottom:0}.form-inline .form-group{flex:0 0 auto;flex-flow:row wrap}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;text-align:center;white-space:nowrap;vertical-align:middle;user-select:none;border:1px solid transparent;padding:.46875rem 1rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(33,150,243,.25)}.btn.disabled,.btn:disabled{opacity:.65;box-shadow:none}.btn:not(:disabled):not(.disabled){cursor:pointer}.btn:not(:disabled):not(.disabled).active,.btn:not(:disabled):not(.disabled):active{background-image:none;box-shadow:none}.btn:not(:disabled):not(.disabled).active:focus,.btn:not(:disabled):not(.disabled):active:focus{box-shadow:0 0 0 .2rem rgba(33,150,243,.25),none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#2196f3;border-color:#2196f3;box-shadow:none}.btn-primary:hover{color:#fff;background-color:#0c83e2;border-color:#0c7cd5}.btn-primary.focus,.btn-primary:focus{box-shadow:none,0 0 0 .2rem rgba(33,150,243,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#2196f3;border-color:#2196f3}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0c7cd5;border-color:#0b75c9}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:none,0 0 0 .2rem rgba(33,150,243,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d;box-shadow:none}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:none,0 0 0 .2rem hsla(208,7%,46%,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:none,0 0 0 .2rem hsla(208,7%,46%,.5)}.btn-success{color:#fff;background-color:#4caf50;border-color:#4caf50;box-shadow:none}.btn-success:hover{color:#fff;background-color:#409444;border-color:#3d8b40}.btn-success.focus,.btn-success:focus{box-shadow:none,0 0 0 .2rem rgba(76,175,80,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#4caf50;border-color:#4caf50}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#3d8b40;border-color:#39833c}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:none,0 0 0 .2rem rgba(76,175,80,.5)}.btn-info{color:#fff;background-color:#00bcd4;border-color:#00bcd4;box-shadow:none}.btn-info:hover{color:#fff;background-color:#009aae;border-color:#008fa1}.btn-info.focus,.btn-info:focus{box-shadow:none,0 0 0 .2rem rgba(0,188,212,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#00bcd4;border-color:#00bcd4}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#008fa1;border-color:#008394}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:none,0 0 0 .2rem rgba(0,188,212,.5)}.btn-warning{color:#212529;background-color:#ffeb3b;border-color:#ffeb3b;box-shadow:none}.btn-warning:hover{color:#212529;background-color:#ffe715;border-color:#ffe608}.btn-warning.focus,.btn-warning:focus{box-shadow:none,0 0 0 .2rem rgba(255,235,59,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffeb3b;border-color:#ffeb3b}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#ffe608;border-color:#fae100}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:none,0 0 0 .2rem rgba(255,235,59,.5)}.btn-danger{color:#fff;background-color:#f44336;border-color:#f44336;box-shadow:none}.btn-danger:hover{color:#fff;background-color:#f22112;border-color:#ea1c0d}.btn-danger.focus,.btn-danger:focus{box-shadow:none,0 0 0 .2rem rgba(244,67,54,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#f44336;border-color:#f44336}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#ea1c0d;border-color:#de1b0c}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:none,0 0 0 .2rem rgba(244,67,54,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa;box-shadow:none}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:none,0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:none,0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40;box-shadow:none}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:none,0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:none,0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#2196f3;background-color:transparent;background-image:none;border-color:#2196f3}.btn-outline-primary:hover{color:#fff;background-color:#2196f3;border-color:#2196f3}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(33,150,243,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#2196f3;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#2196f3;border-color:#2196f3}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(33,150,243,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem hsla(208,7%,46%,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem hsla(208,7%,46%,.5)}.btn-outline-success{color:#4caf50;background-color:transparent;background-image:none;border-color:#4caf50}.btn-outline-success:hover{color:#fff;background-color:#4caf50;border-color:#4caf50}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(76,175,80,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#4caf50;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#4caf50;border-color:#4caf50}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(76,175,80,.5)}.btn-outline-info{color:#00bcd4;background-color:transparent;background-image:none;border-color:#00bcd4}.btn-outline-info:hover{color:#fff;background-color:#00bcd4;border-color:#00bcd4}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(0,188,212,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#00bcd4;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#00bcd4;border-color:#00bcd4}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,188,212,.5)}.btn-outline-warning{color:#ffeb3b;background-color:transparent;background-image:none;border-color:#ffeb3b}.btn-outline-warning:hover{color:#212529;background-color:#ffeb3b;border-color:#ffeb3b}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,235,59,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffeb3b;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffeb3b;border-color:#ffeb3b}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,235,59,.5)}.btn-outline-danger{color:#f44336;background-color:transparent;background-image:none;border-color:#f44336}.btn-outline-danger:hover{color:#fff;background-color:#f44336;border-color:#f44336}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(244,67,54,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#f44336;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#f44336;border-color:#f44336}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(244,67,54,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#9c27b0;background-color:transparent}.btn-link:hover{color:#0a6ebd;background-color:transparent}.btn-link.focus,.btn-link:focus,.btn-link:hover{text-decoration:underline;border-color:transparent}.btn-link.focus,.btn-link:focus{box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#999}.btn-group-lg>.btn,.btn-lg{padding:1.125rem 2.25rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.40625rem 1.25rem;font-size:.875rem;line-height:1.5;border-radius:.1875rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{height:0;overflow:hidden;transition:height .35s ease}.collapsing,.dropdown,.dropup{position:relative}.dropdown-toggle:after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty:after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.dropup .dropdown-menu{margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle:after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty:after{margin-left:0}.dropright .dropdown-menu{margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle:after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty:after{margin-left:0}.dropright .dropdown-toggle:after{vertical-align:0}.dropleft .dropdown-menu{margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle:after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";display:none}.dropleft .dropdown-toggle:before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty:after{margin-left:0}.dropleft .dropdown-toggle:before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.625rem 1.25rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#2196f3}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.25rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.btn-group,.btn-group-vertical{display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:0 1 auto}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group,.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.dropdown-toggle-split:after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.9375rem;padding-left:.9375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.6875rem;padding-left:1.6875rem}.btn-group.show .dropdown-toggle,.btn-group.show .dropdown-toggle.btn-link{box-shadow:none}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio],.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file:focus,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label:before{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label,.input-group>.custom-file:not(:first-child) .custom-file-label:before{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:.4375rem 0;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:transparent;border:1px solid transparent;border-radius:0}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label:before{color:#fff;background-color:#2196f3;box-shadow:none}.custom-control-input:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem rgba(33,150,243,.25)}.custom-control-input:active~.custom-control-label:before{color:#fff;background-color:#cae6fc;box-shadow:none}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label:before{background-color:#e9ecef}.custom-control-label{margin-bottom:0}.custom-control-label:before{pointer-events:none;user-select:none;background-color:#dee2e6;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-control-label:after,.custom-control-label:before{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;content:""}.custom-control-label:after{background-repeat:no-repeat;background-position:50%;background-size:50% 50%}.custom-checkbox .custom-control-label:before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label:before{background-color:#2196f3}.custom-checkbox .custom-control-input:checked~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label:before{background-color:#2196f3;box-shadow:none}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23ffffff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(33,150,243,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label:before{background-color:rgba(33,150,243,.5)}.custom-radio .custom-control-label:before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label:before{background-color:#2196f3}.custom-radio .custom-control-input:checked~.custom-control-label:after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23ffffff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label:before{background-color:rgba(33,150,243,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.4375rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #d2d2d2;border-radius:.25rem;appearance:none}.custom-select:focus{border-color:#9acffa;outline:0;box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(154,207,250,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:transparent}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(2.125rem + 2px);font-size:75%}.custom-select-lg,.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem}.custom-select-lg{height:calc(4.125rem + 2px);font-size:125%}.custom-file{display:inline-block;margin-bottom:0}.custom-file,.custom-file-input{position:relative;width:100%;height:calc(2.4375rem + 2px)}.custom-file-input{z-index:2;margin:0;opacity:0}.custom-file-input:focus~.custom-file-control{border-color:#9acffa;box-shadow:0 0 0 .2rem rgba(33,150,243,.25)}.custom-file-input:focus~.custom-file-control:before{border-color:#9acffa}.custom-file-input:lang(en)~.custom-file-label:after{content:"Browse"}.custom-file-label{left:0;z-index:1;height:calc(2.4375rem + 2px);border:0 solid #d2d2d2;border-radius:0;box-shadow:none}.custom-file-label,.custom-file-label:after{position:absolute;top:0;right:0;padding:.46875rem 1rem;line-height:1.3;color:#495057;background-color:transparent}.custom-file-label:after{bottom:0;z-index:3;display:block;height:calc((2.4375rem + 2px) - 0 * 2);content:"Browse";border-left:0 solid #d2d2d2;border-radius:0 0 0 0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fafafa;border-color:#dee2e6 #dee2e6 #fafafa}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#2196f3}.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;padding:.5rem 1rem}.navbar,.navbar>.container,.navbar>.container-fluid{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat 50%;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .dropup .dropdown-menu{top:auto;bottom:100%}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .dropup .dropdown-menu{top:auto;bottom:100%}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .dropdown-menu-right{right:0;left:auto}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .dropup .dropdown-menu{top:auto;bottom:100%}.navbar-light .navbar-brand,.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand,.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:hsla(0,0%,100%,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:hsla(0,0%,100%,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:hsla(0,0%,100%,.5);border-color:hsla(0,0%,100%,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:hsla(0,0%,100%,.5)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid #eee;border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem}.card-subtitle,.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#fff;border-bottom:1px solid #eee}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:#fff;border-top:1px solid #eee}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-bottom:-.75rem;border-bottom:0}.card-header-pills,.card-header-tabs{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:flex;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:flex;flex:1 0 0%;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:flex;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child),.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{column-count:3;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%}}.breadcrumb{display:flex;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item:before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover:before{text-decoration:underline;text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:0;line-height:1.25;color:#2196f3;background-color:transparent;border:0 solid #dee2e6}.page-link:hover{color:#0a6ebd;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(33,150,243,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#2196f3;border-color:#2196f3}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:transparent;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 0;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem 0;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:500}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#114e7e;background-color:#d3eafd;border-color:#c1e2fc}.alert-primary hr{border-top-color:#a9d7fb}.alert-primary .alert-link{color:#0b3251}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#285b2a;background-color:#dbefdc;border-color:#cde9ce}.alert-success hr{border-top-color:#bbe1bd}.alert-success .alert-link{color:#18381a}.alert-info{color:#00626e;background-color:#ccf2f6;border-color:#b8ecf3}.alert-info hr{border-top-color:#a2e6ef}.alert-info .alert-link{color:#00353b}.alert-warning{color:#857a1f;background-color:#fffbd8;border-color:#fff9c8}.alert-warning hr{border-top-color:#fff6af}.alert-warning .alert-link{color:#5c5415}.alert-danger{color:#7f231c;background-color:#fdd9d7;border-color:#fccac7}.alert-danger hr{border-top-color:#fbb3af}.alert-danger .alert-link{color:#551713}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@keyframes a{0%{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem;box-shadow:inset 0 .1rem .1rem rgba(0,0,0,.1)}.progress-bar{display:flex;flex-direction:column;justify-content:center;color:#fff;text-align:center;background-color:#2196f3;transition:width .6s ease}.progress-bar-striped{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:a 1s linear infinite}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:0;background-color:inherit;border:0 solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:inherit}.list-group-item.active{z-index:2;color:#fff;background-color:#2196f3;border-color:#2196f3}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#114e7e;background-color:#c1e2fc}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#114e7e;background-color:#a9d7fb}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#114e7e;border-color:#114e7e}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#285b2a;background-color:#cde9ce}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#285b2a;background-color:#bbe1bd}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#285b2a;border-color:#285b2a}.list-group-item-info{color:#00626e;background-color:#b8ecf3}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#00626e;background-color:#a2e6ef}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#00626e;border-color:#00626e}.list-group-item-warning{color:#857a1f;background-color:#fff9c8}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#857a1f;background-color:#fff6af}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#857a1f;border-color:#857a1f}.list-group-item-danger{color:#7f231c;background-color:#fccac7}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#7f231c;background-color:#fbb3af}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#7f231c;border-color:#7f231c}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:500;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;opacity:.75}.close:not(:disabled):not(.disabled){cursor:pointer}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:500;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#2196f3}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0c7cd5}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#4caf50}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#3d8b40}.badge-info{color:#fff;background-color:#00bcd4}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#008fa1}.badge-warning{color:#212529;background-color:#ffeb3b}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#ffe608}.badge-danger{color:#fff;background-color:#f44336}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#ea1c0d}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.modal,.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;outline:0}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translateY(-25%)}.modal.show .modal-dialog{transform:translate(0)}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;box-shadow:0 .25rem .5rem rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.26}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;align-items:center;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-content{box-shadow:0 .5rem 1rem rgba(0,0,0,.5)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:Roboto,Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;word-wrap:break-word}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow:before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow:before,.bs-tooltip-top .arrow:before{top:0;border-width:.4rem .4rem 0;border-top-color:rgba(97,97,97,.9)}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow:before,.bs-tooltip-right .arrow:before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:rgba(97,97,97,.9)}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow:before,.bs-tooltip-bottom .arrow:before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:rgba(97,97,97,.9)}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow:before,.bs-tooltip-left .arrow:before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:rgba(97,97,97,.9)}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:rgba(97,97,97,.9);border-radius:.25rem}.popover{top:0;left:0;z-index:1060;max-width:276px;font-family:Roboto,Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;box-shadow:0 .25rem .5rem rgba(0,0,0,.2)}.popover,.popover .arrow{position:absolute;display:block}.popover .arrow{width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow:after,.popover .arrow:before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow:after,.bs-popover-auto[x-placement^=top] .arrow:before,.bs-popover-top .arrow:after,.bs-popover-top .arrow:before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow:before,.bs-popover-top .arrow:before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow:after,.bs-popover-top .arrow:after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow:after,.bs-popover-auto[x-placement^=right] .arrow:before,.bs-popover-right .arrow:after,.bs-popover-right .arrow:before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow:before,.bs-popover-right .arrow:before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow:after,.bs-popover-right .arrow:after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow:after,.bs-popover-auto[x-placement^=bottom] .arrow:before,.bs-popover-bottom .arrow:after,.bs-popover-bottom .arrow:before{border-width:0 .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow:before,.bs-popover-bottom .arrow:before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow:after,.bs-popover-bottom .arrow:after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header:before,.bs-popover-bottom .popover-header:before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow:after,.bs-popover-auto[x-placement^=left] .arrow:before,.bs-popover-left .arrow:after,.bs-popover-left .arrow:before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow:before,.bs-popover-left .arrow:before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow:after,.bs-popover-left .arrow:after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;align-items:center;width:100%;transition:transform .6s ease;backface-visibility:hidden;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{transform:translateX(0)}@supports (transform-style:preserve-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{transform:translateZ(0)}}.active.carousel-item-right,.carousel-item-next{transform:translateX(100%)}@supports (transform-style:preserve-3d){.active.carousel-item-right,.carousel-item-next{transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{transform:translateX(-100%)}@supports (transform-style:preserve-3d){.active.carousel-item-left,.carousel-item-prev{transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat 50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;background-color:hsla(0,0%,100%,.5)}.carousel-indicators li:before{top:-10px}.carousel-indicators li:after,.carousel-indicators li:before{position:absolute;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li:after{bottom:-10px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#2196f3!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0c7cd5!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#4caf50!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#3d8b40!important}.bg-info{background-color:#00bcd4!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#008fa1!important}.bg-warning{background-color:#ffeb3b!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#ffe608!important}.bg-danger{background-color:#f44336!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#ea1c0d!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#2196f3!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#4caf50!important}.border-info{border-color:#00bcd4!important}.border-warning{border-color:#ffeb3b!important}.border-danger{border-color:#f44336!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important}.rounded-right,.rounded-top{border-top-right-radius:.25rem!important}.rounded-bottom,.rounded-right{border-bottom-right-radius:.25rem!important}.rounded-bottom,.rounded-left{border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix:after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive:before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9:before{padding-top:42.857143%}.embed-responsive-16by9:before{padding-top:56.25%}.embed-responsive-4by3:before{padding-top:75%}.embed-responsive-1by1:before{padding-top:100%}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}@media (min-width:576px){.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}}@media (min-width:768px){.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.fixed-top{top:0}.fixed-bottom,.fixed-top{position:fixed;right:0;left:0;z-index:1030}.fixed-bottom{bottom:0}@supports (position:sticky){.sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;clip-path:inset(50%);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal;clip-path:none}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:500!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#2196f3!important}a.text-primary:focus,a.text-primary:hover{color:#0c7cd5!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}a.text-success:focus,a.text-success:hover{color:#3d8b40!important}a.text-info:focus,a.text-info:hover{color:#008fa1!important}.text-warning{color:#ffeb3b!important}a.text-warning:focus,a.text-warning:hover{color:#ffe608!important}a.text-danger:focus,a.text-danger:hover{color:#ea1c0d!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.bmd-help,.text-muted{color:#6c757d!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.btn{position:relative;padding:12px 30px;margin:.3125rem 1px;font-size:.75rem;font-weight:400;line-height:1.428571;text-decoration:none;text-transform:uppercase;letter-spacing:0;cursor:pointer;background-color:transparent;border:0;border-radius:.2rem;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1);will-change:box-shadow,transform}.btn,.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:0}.btn.btn-primary{color:#fff;background-color:#9c27b0;border-color:#9c27b0;box-shadow:0 2px 2px 0 rgba(156,39,176,.14),0 3px 1px -2px rgba(156,39,176,.2),0 1px 5px 0 rgba(156,39,176,.12)}.btn.btn-primary.focus,.btn.btn-primary:focus,.btn.btn-primary:hover{color:#fff;background-color:#9124a3;border-color:#701c7e}.btn.btn-primary.active,.btn.btn-primary:active,.open>.btn.btn-primary.dropdown-toggle,.show>.btn.btn-primary.dropdown-toggle{color:#fff;background-color:#9124a3;border-color:#701c7e;box-shadow:0 2px 2px 0 rgba(156,39,176,.14),0 3px 1px -2px rgba(156,39,176,.2),0 1px 5px 0 rgba(156,39,176,.12)}.btn.btn-primary.active.focus,.btn.btn-primary.active:focus,.btn.btn-primary.active:hover,.btn.btn-primary:active.focus,.btn.btn-primary:active:focus,.btn.btn-primary:active:hover,.open>.btn.btn-primary.dropdown-toggle.focus,.open>.btn.btn-primary.dropdown-toggle:focus,.open>.btn.btn-primary.dropdown-toggle:hover,.show>.btn.btn-primary.dropdown-toggle.focus,.show>.btn.btn-primary.dropdown-toggle:focus,.show>.btn.btn-primary.dropdown-toggle:hover{color:#fff;background-color:#9124a3;border-color:#3f1048}.open>.btn.btn-primary.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#9c27b0}.open>.btn.btn-primary.dropdown-toggle.bmd-btn-icon:hover{background-color:#9124a3}.btn.btn-primary.disabled.focus,.btn.btn-primary.disabled:focus,.btn.btn-primary.disabled:hover,.btn.btn-primary:disabled.focus,.btn.btn-primary:disabled:focus,.btn.btn-primary:disabled:hover{background-color:#9c27b0;border-color:#9c27b0}.btn.btn-primary:active,.btn.btn-primary:focus,.btn.btn-primary:hover{box-shadow:0 14px 26px -12px rgba(156,39,176,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(156,39,176,.2)}.btn.btn-primary.btn-link{box-shadow:none}.btn.btn-primary.btn-link,.btn.btn-primary.btn-link:active,.btn.btn-primary.btn-link:focus,.btn.btn-primary.btn-link:hover{background-color:transparent;color:#9c27b0}.btn.btn-secondary{color:#333;background-color:#fafafa;border-color:#ccc;box-shadow:0 2px 2px 0 hsla(0,0%,98%,.14),0 3px 1px -2px hsla(0,0%,98%,.2),0 1px 5px 0 hsla(0,0%,98%,.12)}.btn.btn-secondary.focus,.btn.btn-secondary:focus,.btn.btn-secondary:hover{color:#333;background-color:#f2f2f2;border-color:#adadad}.btn.btn-secondary.active,.btn.btn-secondary:active,.open>.btn.btn-secondary.dropdown-toggle,.show>.btn.btn-secondary.dropdown-toggle{color:#333;background-color:#f2f2f2;border-color:#adadad;box-shadow:0 2px 2px 0 hsla(0,0%,98%,.14),0 3px 1px -2px hsla(0,0%,98%,.2),0 1px 5px 0 hsla(0,0%,98%,.12)}.btn.btn-secondary.active.focus,.btn.btn-secondary.active:focus,.btn.btn-secondary.active:hover,.btn.btn-secondary:active.focus,.btn.btn-secondary:active:focus,.btn.btn-secondary:active:hover,.open>.btn.btn-secondary.dropdown-toggle.focus,.open>.btn.btn-secondary.dropdown-toggle:focus,.open>.btn.btn-secondary.dropdown-toggle:hover,.show>.btn.btn-secondary.dropdown-toggle.focus,.show>.btn.btn-secondary.dropdown-toggle:focus,.show>.btn.btn-secondary.dropdown-toggle:hover{color:#333;background-color:#f2f2f2;border-color:#8c8c8c}.open>.btn.btn-secondary.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#fafafa}.open>.btn.btn-secondary.dropdown-toggle.bmd-btn-icon:hover{background-color:#f2f2f2}.btn.btn-secondary.disabled.focus,.btn.btn-secondary.disabled:focus,.btn.btn-secondary.disabled:hover,.btn.btn-secondary:disabled.focus,.btn.btn-secondary:disabled:focus,.btn.btn-secondary:disabled:hover{background-color:#fafafa;border-color:#ccc}.btn.btn-secondary:active,.btn.btn-secondary:focus,.btn.btn-secondary:hover{box-shadow:0 14px 26px -12px hsla(0,0%,98%,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px hsla(0,0%,98%,.2)}.btn.btn-secondary.btn-link{box-shadow:none}.btn.btn-secondary.btn-link,.btn.btn-secondary.btn-link:active,.btn.btn-secondary.btn-link:focus,.btn.btn-secondary.btn-link:hover{background-color:transparent;color:#fafafa}.btn.btn-info{color:#fff;background-color:#00bcd4;border-color:#00bcd4;box-shadow:0 2px 2px 0 rgba(0,188,212,.14),0 3px 1px -2px rgba(0,188,212,.2),0 1px 5px 0 rgba(0,188,212,.12)}.btn.btn-info.focus,.btn.btn-info:focus,.btn.btn-info:hover{color:#fff;background-color:#00aec5;border-color:#008697}.btn.btn-info.active,.btn.btn-info:active,.open>.btn.btn-info.dropdown-toggle,.show>.btn.btn-info.dropdown-toggle{color:#fff;background-color:#00aec5;border-color:#008697;box-shadow:0 2px 2px 0 rgba(0,188,212,.14),0 3px 1px -2px rgba(0,188,212,.2),0 1px 5px 0 rgba(0,188,212,.12)}.btn.btn-info.active.focus,.btn.btn-info.active:focus,.btn.btn-info.active:hover,.btn.btn-info:active.focus,.btn.btn-info:active:focus,.btn.btn-info:active:hover,.open>.btn.btn-info.dropdown-toggle.focus,.open>.btn.btn-info.dropdown-toggle:focus,.open>.btn.btn-info.dropdown-toggle:hover,.show>.btn.btn-info.dropdown-toggle.focus,.show>.btn.btn-info.dropdown-toggle:focus,.show>.btn.btn-info.dropdown-toggle:hover{color:#fff;background-color:#00aec5;border-color:#004b55}.open>.btn.btn-info.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#00bcd4}.open>.btn.btn-info.dropdown-toggle.bmd-btn-icon:hover{background-color:#00aec5}.btn.btn-info.disabled.focus,.btn.btn-info.disabled:focus,.btn.btn-info.disabled:hover,.btn.btn-info:disabled.focus,.btn.btn-info:disabled:focus,.btn.btn-info:disabled:hover{background-color:#00bcd4;border-color:#00bcd4}.btn.btn-info:active,.btn.btn-info:focus,.btn.btn-info:hover{box-shadow:0 14px 26px -12px rgba(0,188,212,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(0,188,212,.2)}.btn.btn-info.btn-link{box-shadow:none}.btn.btn-info.btn-link,.btn.btn-info.btn-link:active,.btn.btn-info.btn-link:focus,.btn.btn-info.btn-link:hover{background-color:transparent;color:#00bcd4}.btn.btn-success{color:#fff;background-color:#4caf50;border-color:#4caf50;box-shadow:0 2px 2px 0 rgba(76,175,80,.14),0 3px 1px -2px rgba(76,175,80,.2),0 1px 5px 0 rgba(76,175,80,.12)}.btn.btn-success.focus,.btn.btn-success:focus,.btn.btn-success:hover{color:#fff;background-color:#47a44b;border-color:#39843c}.btn.btn-success.active,.btn.btn-success:active,.open>.btn.btn-success.dropdown-toggle,.show>.btn.btn-success.dropdown-toggle{color:#fff;background-color:#47a44b;border-color:#39843c;box-shadow:0 2px 2px 0 rgba(76,175,80,.14),0 3px 1px -2px rgba(76,175,80,.2),0 1px 5px 0 rgba(76,175,80,.12)}.btn.btn-success.active.focus,.btn.btn-success.active:focus,.btn.btn-success.active:hover,.btn.btn-success:active.focus,.btn.btn-success:active:focus,.btn.btn-success:active:hover,.open>.btn.btn-success.dropdown-toggle.focus,.open>.btn.btn-success.dropdown-toggle:focus,.open>.btn.btn-success.dropdown-toggle:hover,.show>.btn.btn-success.dropdown-toggle.focus,.show>.btn.btn-success.dropdown-toggle:focus,.show>.btn.btn-success.dropdown-toggle:hover{color:#fff;background-color:#47a44b;border-color:#255627}.open>.btn.btn-success.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#4caf50}.open>.btn.btn-success.dropdown-toggle.bmd-btn-icon:hover{background-color:#47a44b}.btn.btn-success.disabled.focus,.btn.btn-success.disabled:focus,.btn.btn-success.disabled:hover,.btn.btn-success:disabled.focus,.btn.btn-success:disabled:focus,.btn.btn-success:disabled:hover{background-color:#4caf50;border-color:#4caf50}.btn.btn-success:active,.btn.btn-success:focus,.btn.btn-success:hover{box-shadow:0 14px 26px -12px rgba(76,175,80,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(76,175,80,.2)}.btn.btn-success.btn-link{box-shadow:none}.btn.btn-success.btn-link,.btn.btn-success.btn-link:active,.btn.btn-success.btn-link:focus,.btn.btn-success.btn-link:hover{background-color:transparent;color:#4caf50}.btn.btn-warning{color:#fff;background-color:#ff9800;border-color:#ff9800;box-shadow:0 2px 2px 0 rgba(255,152,0,.14),0 3px 1px -2px rgba(255,152,0,.2),0 1px 5px 0 rgba(255,152,0,.12)}.btn.btn-warning.focus,.btn.btn-warning:focus,.btn.btn-warning:hover{color:#fff;background-color:#f08f00;border-color:#c27400}.btn.btn-warning.active,.btn.btn-warning:active,.open>.btn.btn-warning.dropdown-toggle,.show>.btn.btn-warning.dropdown-toggle{color:#fff;background-color:#f08f00;border-color:#c27400;box-shadow:0 2px 2px 0 rgba(255,152,0,.14),0 3px 1px -2px rgba(255,152,0,.2),0 1px 5px 0 rgba(255,152,0,.12)}.btn.btn-warning.active.focus,.btn.btn-warning.active:focus,.btn.btn-warning.active:hover,.btn.btn-warning:active.focus,.btn.btn-warning:active:focus,.btn.btn-warning:active:hover,.open>.btn.btn-warning.dropdown-toggle.focus,.open>.btn.btn-warning.dropdown-toggle:focus,.open>.btn.btn-warning.dropdown-toggle:hover,.show>.btn.btn-warning.dropdown-toggle.focus,.show>.btn.btn-warning.dropdown-toggle:focus,.show>.btn.btn-warning.dropdown-toggle:hover{color:#fff;background-color:#f08f00;border-color:#804c00}.open>.btn.btn-warning.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#ff9800}.open>.btn.btn-warning.dropdown-toggle.bmd-btn-icon:hover{background-color:#f08f00}.btn.btn-warning.disabled.focus,.btn.btn-warning.disabled:focus,.btn.btn-warning.disabled:hover,.btn.btn-warning:disabled.focus,.btn.btn-warning:disabled:focus,.btn.btn-warning:disabled:hover{background-color:#ff9800;border-color:#ff9800}.btn.btn-warning:active,.btn.btn-warning:focus,.btn.btn-warning:hover{box-shadow:0 14px 26px -12px rgba(255,152,0,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(255,152,0,.2)}.btn.btn-warning.btn-link{box-shadow:none}.btn.btn-warning.btn-link,.btn.btn-warning.btn-link:active,.btn.btn-warning.btn-link:focus,.btn.btn-warning.btn-link:hover{background-color:transparent;color:#ff9800}.btn.btn-danger{color:#fff;background-color:#f44336;border-color:#f44336;box-shadow:0 2px 2px 0 rgba(244,67,54,.14),0 3px 1px -2px rgba(244,67,54,.2),0 1px 5px 0 rgba(244,67,54,.12)}.btn.btn-danger.focus,.btn.btn-danger:focus,.btn.btn-danger:hover{color:#fff;background-color:#f33527;border-color:#e11b0c}.btn.btn-danger.active,.btn.btn-danger:active,.open>.btn.btn-danger.dropdown-toggle,.show>.btn.btn-danger.dropdown-toggle{color:#fff;background-color:#f33527;border-color:#e11b0c;box-shadow:0 2px 2px 0 rgba(244,67,54,.14),0 3px 1px -2px rgba(244,67,54,.2),0 1px 5px 0 rgba(244,67,54,.12)}.btn.btn-danger.active.focus,.btn.btn-danger.active:focus,.btn.btn-danger.active:hover,.btn.btn-danger:active.focus,.btn.btn-danger:active:focus,.btn.btn-danger:active:hover,.open>.btn.btn-danger.dropdown-toggle.focus,.open>.btn.btn-danger.dropdown-toggle:focus,.open>.btn.btn-danger.dropdown-toggle:hover,.show>.btn.btn-danger.dropdown-toggle.focus,.show>.btn.btn-danger.dropdown-toggle:focus,.show>.btn.btn-danger.dropdown-toggle:hover{color:#fff;background-color:#f33527;border-color:#a21309}.open>.btn.btn-danger.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#f44336}.open>.btn.btn-danger.dropdown-toggle.bmd-btn-icon:hover{background-color:#f33527}.btn.btn-danger.disabled.focus,.btn.btn-danger.disabled:focus,.btn.btn-danger.disabled:hover,.btn.btn-danger:disabled.focus,.btn.btn-danger:disabled:focus,.btn.btn-danger:disabled:hover{background-color:#f44336;border-color:#f44336}.btn.btn-danger:active,.btn.btn-danger:focus,.btn.btn-danger:hover{box-shadow:0 14px 26px -12px rgba(244,67,54,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(244,67,54,.2)}.btn.btn-danger.btn-link{box-shadow:none}.btn.btn-danger.btn-link,.btn.btn-danger.btn-link:active,.btn.btn-danger.btn-link:focus,.btn.btn-danger.btn-link:hover{background-color:transparent;color:#f44336}.btn.btn-rose{color:#fff;background-color:#e91e63;border-color:#e91e63;box-shadow:0 2px 2px 0 rgba(233,30,99,.14),0 3px 1px -2px rgba(233,30,99,.2),0 1px 5px 0 rgba(233,30,99,.12)}.btn.btn-rose.focus,.btn.btn-rose:focus,.btn.btn-rose:hover{color:#fff;background-color:#ea2c6d;border-color:#b8124a}.btn.btn-rose.active,.btn.btn-rose:active,.open>.btn.btn-rose.dropdown-toggle,.show>.btn.btn-rose.dropdown-toggle{color:#fff;background-color:#ea2c6d;border-color:#b8124a;box-shadow:0 2px 2px 0 rgba(233,30,99,.14),0 3px 1px -2px rgba(233,30,99,.2),0 1px 5px 0 rgba(233,30,99,.12)}.btn.btn-rose.active.focus,.btn.btn-rose.active:focus,.btn.btn-rose.active:hover,.btn.btn-rose:active.focus,.btn.btn-rose:active:focus,.btn.btn-rose:active:hover,.open>.btn.btn-rose.dropdown-toggle.focus,.open>.btn.btn-rose.dropdown-toggle:focus,.open>.btn.btn-rose.dropdown-toggle:hover,.show>.btn.btn-rose.dropdown-toggle.focus,.show>.btn.btn-rose.dropdown-toggle:focus,.show>.btn.btn-rose.dropdown-toggle:hover{color:#fff;background-color:#ea2c6d;border-color:#7b0c32}.open>.btn.btn-rose.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#e91e63}.open>.btn.btn-rose.dropdown-toggle.bmd-btn-icon:hover{background-color:#ea2c6d}.btn.btn-rose.disabled.focus,.btn.btn-rose.disabled:focus,.btn.btn-rose.disabled:hover,.btn.btn-rose:disabled.focus,.btn.btn-rose:disabled:focus,.btn.btn-rose:disabled:hover{background-color:#e91e63;border-color:#e91e63}.btn.btn-rose:active,.btn.btn-rose:focus,.btn.btn-rose:hover{box-shadow:0 14px 26px -12px rgba(233,30,99,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(233,30,99,.2)}.btn.btn-rose.btn-link{box-shadow:none}.btn.btn-rose.btn-link,.btn.btn-rose.btn-link:active,.btn.btn-rose.btn-link:focus,.btn.btn-rose.btn-link:hover{background-color:transparent;color:#e91e63}.btn,.btn.btn-default{color:#fff;background-color:#999;border-color:#999;box-shadow:0 2px 2px 0 hsla(0,0%,60%,.14),0 3px 1px -2px hsla(0,0%,60%,.2),0 1px 5px 0 hsla(0,0%,60%,.12)}.btn.btn-default.focus,.btn.btn-default:focus,.btn.btn-default:hover,.btn.focus,.btn:focus,.btn:hover{color:#fff;background-color:#919191;border-color:#7a7a7a}.btn.active,.btn.btn-default.active,.btn.btn-default:active,.btn:active,.open>.btn.btn-default.dropdown-toggle,.open>.btn.dropdown-toggle,.show>.btn.btn-default.dropdown-toggle,.show>.btn.dropdown-toggle{color:#fff;background-color:#919191;border-color:#7a7a7a;box-shadow:0 2px 2px 0 hsla(0,0%,60%,.14),0 3px 1px -2px hsla(0,0%,60%,.2),0 1px 5px 0 hsla(0,0%,60%,.12)}.btn.active.focus,.btn.active:focus,.btn.active:hover,.btn.btn-default.active.focus,.btn.btn-default.active:focus,.btn.btn-default.active:hover,.btn.btn-default:active.focus,.btn.btn-default:active:focus,.btn.btn-default:active:hover,.btn:active.focus,.btn:active:focus,.btn:active:hover,.open>.btn.btn-default.dropdown-toggle.focus,.open>.btn.btn-default.dropdown-toggle:focus,.open>.btn.btn-default.dropdown-toggle:hover,.open>.btn.dropdown-toggle.focus,.open>.btn.dropdown-toggle:focus,.open>.btn.dropdown-toggle:hover,.show>.btn.btn-default.dropdown-toggle.focus,.show>.btn.btn-default.dropdown-toggle:focus,.show>.btn.btn-default.dropdown-toggle:hover,.show>.btn.dropdown-toggle.focus,.show>.btn.dropdown-toggle:focus,.show>.btn.dropdown-toggle:hover{color:#fff;background-color:#919191;border-color:#595959}.open>.btn.btn-default.dropdown-toggle.bmd-btn-icon,.open>.btn.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#999}.open>.btn.btn-default.dropdown-toggle.bmd-btn-icon:hover,.open>.btn.dropdown-toggle.bmd-btn-icon:hover{background-color:#919191}.btn.btn-default.disabled.focus,.btn.btn-default.disabled:focus,.btn.btn-default.disabled:hover,.btn.btn-default:disabled.focus,.btn.btn-default:disabled:focus,.btn.btn-default:disabled:hover,.btn.disabled.focus,.btn.disabled:focus,.btn.disabled:hover,.btn:disabled.focus,.btn:disabled:focus,.btn:disabled:hover{background-color:#999;border-color:#999}.btn.btn-default:active,.btn.btn-default:focus,.btn.btn-default:hover,.btn:active,.btn:focus,.btn:hover{box-shadow:0 14px 26px -12px hsla(0,0%,60%,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px hsla(0,0%,60%,.2)}.btn.btn-default.btn-link,.btn.btn-link{background-color:transparent;color:#999;box-shadow:none}.btn.btn-default.btn-link:active,.btn.btn-default.btn-link:focus,.btn.btn-default.btn-link:hover,.btn.btn-link:active,.btn.btn-link:focus,.btn.btn-link:hover{background-color:transparent;color:#999}.btn.btn-white,.btn.btn-white:focus,.btn.btn-white:hover{background-color:#fff;color:#999}.btn.btn-white.btn-link{color:#fff;background:transparent;box-shadow:none}.btn.btn-link:active,.btn.btn-link:focus,.btn.btn-link:hover{text-decoration:none!important}.btn-group-raised .btn.btn-link,.btn-group-raised .btn.btn-link.active,.btn-group-raised .btn.btn-link:active,.btn-group-raised .btn.btn-link:focus,.btn-group-raised .btn.btn-link:hover,.btn-group-raised .btn.disabled,.btn-group-raised .btn:disabled,.btn-group-raised .btn[disabled],.btn.btn-raised.btn-link,.btn.btn-raised.btn-link.active,.btn.btn-raised.btn-link:active,.btn.btn-raised.btn-link:focus,.btn.btn-raised.btn-link:hover,.btn.btn-raised.disabled,.btn.btn-raised:disabled,.btn.btn-raised[disabled],fieldset[disabled][disabled] .btn-group-raised .btn,fieldset[disabled][disabled] .btn.btn-raised{box-shadow:none}.btn.btn-outline,.btn.btn-outline-danger,.btn.btn-outline-info,.btn.btn-outline-primary,.btn.btn-outline-secondary,.btn.btn-outline-success,.btn.btn-outline-warning{border:1px solid currentColor}.btn.btn-outline{color:#333;background-color:transparent;border-color:#333}.btn.btn-outline.focus,.btn.btn-outline:focus,.btn.btn-outline:hover{color:#333;background-color:hsla(0,0%,60%,.2);border-color:#333}.btn.btn-outline.active,.btn.btn-outline:active,.open>.btn.btn-outline.dropdown-toggle,.show>.btn.btn-outline.dropdown-toggle{color:#333;background-color:hsla(0,0%,60%,.2);border-color:#333;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.btn.btn-outline.active.focus,.btn.btn-outline.active:focus,.btn.btn-outline.active:hover,.btn.btn-outline:active.focus,.btn.btn-outline:active:focus,.btn.btn-outline:active:hover,.open>.btn.btn-outline.dropdown-toggle.focus,.open>.btn.btn-outline.dropdown-toggle:focus,.open>.btn.btn-outline.dropdown-toggle:hover,.show>.btn.btn-outline.dropdown-toggle.focus,.show>.btn.btn-outline.dropdown-toggle:focus,.show>.btn.btn-outline.dropdown-toggle:hover{color:#333;background-color:hsla(0,0%,60%,.4);border-color:#333}.open>.btn.btn-outline.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.btn.btn-outline.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,60%,.2)}.bg-inverse .btn.btn-outline,.btn.btn-outline.disabled.focus,.btn.btn-outline.disabled:focus,.btn.btn-outline.disabled:hover,.btn.btn-outline:disabled.focus,.btn.btn-outline:disabled:focus,.btn.btn-outline:disabled:hover{background-color:transparent;border-color:#333}.bg-inverse .btn.btn-outline{color:#333}.bg-inverse .btn.btn-outline.focus,.bg-inverse .btn.btn-outline:focus,.bg-inverse .btn.btn-outline:hover{color:#333;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline.active,.bg-inverse .btn.btn-outline:active,.open>.bg-inverse .btn.btn-outline.dropdown-toggle,.show>.bg-inverse .btn.btn-outline.dropdown-toggle{color:#333;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.bg-inverse .btn.btn-outline.active.focus,.bg-inverse .btn.btn-outline.active:focus,.bg-inverse .btn.btn-outline.active:hover,.bg-inverse .btn.btn-outline:active.focus,.bg-inverse .btn.btn-outline:active:focus,.bg-inverse .btn.btn-outline:active:hover,.open>.bg-inverse .btn.btn-outline.dropdown-toggle.focus,.open>.bg-inverse .btn.btn-outline.dropdown-toggle:focus,.open>.bg-inverse .btn.btn-outline.dropdown-toggle:hover,.show>.bg-inverse .btn.btn-outline.dropdown-toggle.focus,.show>.bg-inverse .btn.btn-outline.dropdown-toggle:focus,.show>.bg-inverse .btn.btn-outline.dropdown-toggle:hover{color:#333;background-color:hsla(0,0%,80%,.25);border-color:hsla(0,0%,80%,.25)}.open>.bg-inverse .btn.btn-outline.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.bg-inverse .btn.btn-outline.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline.disabled.focus,.bg-inverse .btn.btn-outline.disabled:focus,.bg-inverse .btn.btn-outline.disabled:hover,.bg-inverse .btn.btn-outline:disabled.focus,.bg-inverse .btn.btn-outline:disabled:focus,.bg-inverse .btn.btn-outline:disabled:hover{background-color:transparent;border-color:#333}.btn.btn-outline.btn-link{background-color:transparent}.btn.btn-outline-primary{color:#9c27b0;background-color:transparent;border-color:#9c27b0}.btn.btn-outline-primary.focus,.btn.btn-outline-primary:focus,.btn.btn-outline-primary:hover{color:#9c27b0;background-color:hsla(0,0%,60%,.2);border-color:#9c27b0}.btn.btn-outline-primary.active,.btn.btn-outline-primary:active,.open>.btn.btn-outline-primary.dropdown-toggle,.show>.btn.btn-outline-primary.dropdown-toggle{color:#9c27b0;background-color:hsla(0,0%,60%,.2);border-color:#9c27b0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.btn.btn-outline-primary.active.focus,.btn.btn-outline-primary.active:focus,.btn.btn-outline-primary.active:hover,.btn.btn-outline-primary:active.focus,.btn.btn-outline-primary:active:focus,.btn.btn-outline-primary:active:hover,.open>.btn.btn-outline-primary.dropdown-toggle.focus,.open>.btn.btn-outline-primary.dropdown-toggle:focus,.open>.btn.btn-outline-primary.dropdown-toggle:hover,.show>.btn.btn-outline-primary.dropdown-toggle.focus,.show>.btn.btn-outline-primary.dropdown-toggle:focus,.show>.btn.btn-outline-primary.dropdown-toggle:hover{color:#9c27b0;background-color:hsla(0,0%,60%,.4);border-color:#9c27b0}.open>.btn.btn-outline-primary.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.btn.btn-outline-primary.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,60%,.2)}.bg-inverse .btn.btn-outline-primary,.btn.btn-outline-primary.disabled.focus,.btn.btn-outline-primary.disabled:focus,.btn.btn-outline-primary.disabled:hover,.btn.btn-outline-primary:disabled.focus,.btn.btn-outline-primary:disabled:focus,.btn.btn-outline-primary:disabled:hover{background-color:transparent;border-color:#9c27b0}.bg-inverse .btn.btn-outline-primary{color:#9c27b0}.bg-inverse .btn.btn-outline-primary.focus,.bg-inverse .btn.btn-outline-primary:focus,.bg-inverse .btn.btn-outline-primary:hover{color:#9c27b0;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-primary.active,.bg-inverse .btn.btn-outline-primary:active,.open>.bg-inverse .btn.btn-outline-primary.dropdown-toggle,.show>.bg-inverse .btn.btn-outline-primary.dropdown-toggle{color:#9c27b0;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.bg-inverse .btn.btn-outline-primary.active.focus,.bg-inverse .btn.btn-outline-primary.active:focus,.bg-inverse .btn.btn-outline-primary.active:hover,.bg-inverse .btn.btn-outline-primary:active.focus,.bg-inverse .btn.btn-outline-primary:active:focus,.bg-inverse .btn.btn-outline-primary:active:hover,.open>.bg-inverse .btn.btn-outline-primary.dropdown-toggle.focus,.open>.bg-inverse .btn.btn-outline-primary.dropdown-toggle:focus,.open>.bg-inverse .btn.btn-outline-primary.dropdown-toggle:hover,.show>.bg-inverse .btn.btn-outline-primary.dropdown-toggle.focus,.show>.bg-inverse .btn.btn-outline-primary.dropdown-toggle:focus,.show>.bg-inverse .btn.btn-outline-primary.dropdown-toggle:hover{color:#9c27b0;background-color:hsla(0,0%,80%,.25);border-color:hsla(0,0%,80%,.25)}.open>.bg-inverse .btn.btn-outline-primary.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.bg-inverse .btn.btn-outline-primary.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-primary.disabled.focus,.bg-inverse .btn.btn-outline-primary.disabled:focus,.bg-inverse .btn.btn-outline-primary.disabled:hover,.bg-inverse .btn.btn-outline-primary:disabled.focus,.bg-inverse .btn.btn-outline-primary:disabled:focus,.bg-inverse .btn.btn-outline-primary:disabled:hover{background-color:transparent;border-color:#9c27b0}.btn.btn-outline-primary.btn-link{background-color:transparent}.btn.btn-outline-secondary{color:#333;background-color:transparent;border-color:#333}.btn.btn-outline-secondary.focus,.btn.btn-outline-secondary:focus,.btn.btn-outline-secondary:hover{color:#333;background-color:hsla(0,0%,60%,.2);border-color:#333}.btn.btn-outline-secondary.active,.btn.btn-outline-secondary:active,.open>.btn.btn-outline-secondary.dropdown-toggle,.show>.btn.btn-outline-secondary.dropdown-toggle{color:#333;background-color:hsla(0,0%,60%,.2);border-color:#333;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.btn.btn-outline-secondary.active.focus,.btn.btn-outline-secondary.active:focus,.btn.btn-outline-secondary.active:hover,.btn.btn-outline-secondary:active.focus,.btn.btn-outline-secondary:active:focus,.btn.btn-outline-secondary:active:hover,.open>.btn.btn-outline-secondary.dropdown-toggle.focus,.open>.btn.btn-outline-secondary.dropdown-toggle:focus,.open>.btn.btn-outline-secondary.dropdown-toggle:hover,.show>.btn.btn-outline-secondary.dropdown-toggle.focus,.show>.btn.btn-outline-secondary.dropdown-toggle:focus,.show>.btn.btn-outline-secondary.dropdown-toggle:hover{color:#333;background-color:hsla(0,0%,60%,.4);border-color:#333}.open>.btn.btn-outline-secondary.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.btn.btn-outline-secondary.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,60%,.2)}.bg-inverse .btn.btn-outline-secondary,.btn.btn-outline-secondary.disabled.focus,.btn.btn-outline-secondary.disabled:focus,.btn.btn-outline-secondary.disabled:hover,.btn.btn-outline-secondary:disabled.focus,.btn.btn-outline-secondary:disabled:focus,.btn.btn-outline-secondary:disabled:hover{background-color:transparent;border-color:#333}.bg-inverse .btn.btn-outline-secondary{color:#333}.bg-inverse .btn.btn-outline-secondary.focus,.bg-inverse .btn.btn-outline-secondary:focus,.bg-inverse .btn.btn-outline-secondary:hover{color:#333;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-secondary.active,.bg-inverse .btn.btn-outline-secondary:active,.open>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle,.show>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle{color:#333;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.bg-inverse .btn.btn-outline-secondary.active.focus,.bg-inverse .btn.btn-outline-secondary.active:focus,.bg-inverse .btn.btn-outline-secondary.active:hover,.bg-inverse .btn.btn-outline-secondary:active.focus,.bg-inverse .btn.btn-outline-secondary:active:focus,.bg-inverse .btn.btn-outline-secondary:active:hover,.open>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle.focus,.open>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle:focus,.open>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle:hover,.show>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle.focus,.show>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle:focus,.show>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle:hover{color:#333;background-color:hsla(0,0%,80%,.25);border-color:hsla(0,0%,80%,.25)}.open>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.bg-inverse .btn.btn-outline-secondary.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-secondary.disabled.focus,.bg-inverse .btn.btn-outline-secondary.disabled:focus,.bg-inverse .btn.btn-outline-secondary.disabled:hover,.bg-inverse .btn.btn-outline-secondary:disabled.focus,.bg-inverse .btn.btn-outline-secondary:disabled:focus,.bg-inverse .btn.btn-outline-secondary:disabled:hover{background-color:transparent;border-color:#333}.btn.btn-outline-secondary.btn-link{background-color:transparent}.btn.btn-outline-info{color:#00bcd4;background-color:transparent;border-color:#00bcd4}.btn.btn-outline-info.focus,.btn.btn-outline-info:focus,.btn.btn-outline-info:hover{color:#00bcd4;background-color:hsla(0,0%,60%,.2);border-color:#00bcd4}.btn.btn-outline-info.active,.btn.btn-outline-info:active,.open>.btn.btn-outline-info.dropdown-toggle,.show>.btn.btn-outline-info.dropdown-toggle{color:#00bcd4;background-color:hsla(0,0%,60%,.2);border-color:#00bcd4;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.btn.btn-outline-info.active.focus,.btn.btn-outline-info.active:focus,.btn.btn-outline-info.active:hover,.btn.btn-outline-info:active.focus,.btn.btn-outline-info:active:focus,.btn.btn-outline-info:active:hover,.open>.btn.btn-outline-info.dropdown-toggle.focus,.open>.btn.btn-outline-info.dropdown-toggle:focus,.open>.btn.btn-outline-info.dropdown-toggle:hover,.show>.btn.btn-outline-info.dropdown-toggle.focus,.show>.btn.btn-outline-info.dropdown-toggle:focus,.show>.btn.btn-outline-info.dropdown-toggle:hover{color:#00bcd4;background-color:hsla(0,0%,60%,.4);border-color:#00bcd4}.open>.btn.btn-outline-info.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.btn.btn-outline-info.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,60%,.2)}.bg-inverse .btn.btn-outline-info,.btn.btn-outline-info.disabled.focus,.btn.btn-outline-info.disabled:focus,.btn.btn-outline-info.disabled:hover,.btn.btn-outline-info:disabled.focus,.btn.btn-outline-info:disabled:focus,.btn.btn-outline-info:disabled:hover{background-color:transparent;border-color:#00bcd4}.bg-inverse .btn.btn-outline-info{color:#00bcd4}.bg-inverse .btn.btn-outline-info.focus,.bg-inverse .btn.btn-outline-info:focus,.bg-inverse .btn.btn-outline-info:hover{color:#00bcd4;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-info.active,.bg-inverse .btn.btn-outline-info:active,.open>.bg-inverse .btn.btn-outline-info.dropdown-toggle,.show>.bg-inverse .btn.btn-outline-info.dropdown-toggle{color:#00bcd4;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.bg-inverse .btn.btn-outline-info.active.focus,.bg-inverse .btn.btn-outline-info.active:focus,.bg-inverse .btn.btn-outline-info.active:hover,.bg-inverse .btn.btn-outline-info:active.focus,.bg-inverse .btn.btn-outline-info:active:focus,.bg-inverse .btn.btn-outline-info:active:hover,.open>.bg-inverse .btn.btn-outline-info.dropdown-toggle.focus,.open>.bg-inverse .btn.btn-outline-info.dropdown-toggle:focus,.open>.bg-inverse .btn.btn-outline-info.dropdown-toggle:hover,.show>.bg-inverse .btn.btn-outline-info.dropdown-toggle.focus,.show>.bg-inverse .btn.btn-outline-info.dropdown-toggle:focus,.show>.bg-inverse .btn.btn-outline-info.dropdown-toggle:hover{color:#00bcd4;background-color:hsla(0,0%,80%,.25);border-color:hsla(0,0%,80%,.25)}.open>.bg-inverse .btn.btn-outline-info.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.bg-inverse .btn.btn-outline-info.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-info.disabled.focus,.bg-inverse .btn.btn-outline-info.disabled:focus,.bg-inverse .btn.btn-outline-info.disabled:hover,.bg-inverse .btn.btn-outline-info:disabled.focus,.bg-inverse .btn.btn-outline-info:disabled:focus,.bg-inverse .btn.btn-outline-info:disabled:hover{background-color:transparent;border-color:#00bcd4}.btn.btn-outline-info.btn-link{background-color:transparent}.btn.btn-outline-success{color:#4caf50;background-color:transparent;border-color:#4caf50}.btn.btn-outline-success.focus,.btn.btn-outline-success:focus,.btn.btn-outline-success:hover{color:#4caf50;background-color:hsla(0,0%,60%,.2);border-color:#4caf50}.btn.btn-outline-success.active,.btn.btn-outline-success:active,.open>.btn.btn-outline-success.dropdown-toggle,.show>.btn.btn-outline-success.dropdown-toggle{color:#4caf50;background-color:hsla(0,0%,60%,.2);border-color:#4caf50;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.btn.btn-outline-success.active.focus,.btn.btn-outline-success.active:focus,.btn.btn-outline-success.active:hover,.btn.btn-outline-success:active.focus,.btn.btn-outline-success:active:focus,.btn.btn-outline-success:active:hover,.open>.btn.btn-outline-success.dropdown-toggle.focus,.open>.btn.btn-outline-success.dropdown-toggle:focus,.open>.btn.btn-outline-success.dropdown-toggle:hover,.show>.btn.btn-outline-success.dropdown-toggle.focus,.show>.btn.btn-outline-success.dropdown-toggle:focus,.show>.btn.btn-outline-success.dropdown-toggle:hover{color:#4caf50;background-color:hsla(0,0%,60%,.4);border-color:#4caf50}.open>.btn.btn-outline-success.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.btn.btn-outline-success.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,60%,.2)}.bg-inverse .btn.btn-outline-success,.btn.btn-outline-success.disabled.focus,.btn.btn-outline-success.disabled:focus,.btn.btn-outline-success.disabled:hover,.btn.btn-outline-success:disabled.focus,.btn.btn-outline-success:disabled:focus,.btn.btn-outline-success:disabled:hover{background-color:transparent;border-color:#4caf50}.bg-inverse .btn.btn-outline-success{color:#4caf50}.bg-inverse .btn.btn-outline-success.focus,.bg-inverse .btn.btn-outline-success:focus,.bg-inverse .btn.btn-outline-success:hover{color:#4caf50;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-success.active,.bg-inverse .btn.btn-outline-success:active,.open>.bg-inverse .btn.btn-outline-success.dropdown-toggle,.show>.bg-inverse .btn.btn-outline-success.dropdown-toggle{color:#4caf50;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.bg-inverse .btn.btn-outline-success.active.focus,.bg-inverse .btn.btn-outline-success.active:focus,.bg-inverse .btn.btn-outline-success.active:hover,.bg-inverse .btn.btn-outline-success:active.focus,.bg-inverse .btn.btn-outline-success:active:focus,.bg-inverse .btn.btn-outline-success:active:hover,.open>.bg-inverse .btn.btn-outline-success.dropdown-toggle.focus,.open>.bg-inverse .btn.btn-outline-success.dropdown-toggle:focus,.open>.bg-inverse .btn.btn-outline-success.dropdown-toggle:hover,.show>.bg-inverse .btn.btn-outline-success.dropdown-toggle.focus,.show>.bg-inverse .btn.btn-outline-success.dropdown-toggle:focus,.show>.bg-inverse .btn.btn-outline-success.dropdown-toggle:hover{color:#4caf50;background-color:hsla(0,0%,80%,.25);border-color:hsla(0,0%,80%,.25)}.open>.bg-inverse .btn.btn-outline-success.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.bg-inverse .btn.btn-outline-success.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-success.disabled.focus,.bg-inverse .btn.btn-outline-success.disabled:focus,.bg-inverse .btn.btn-outline-success.disabled:hover,.bg-inverse .btn.btn-outline-success:disabled.focus,.bg-inverse .btn.btn-outline-success:disabled:focus,.bg-inverse .btn.btn-outline-success:disabled:hover{background-color:transparent;border-color:#4caf50}.btn.btn-outline-success.btn-link{background-color:transparent}.btn.btn-outline-warning{color:#ff9800;background-color:transparent;border-color:#ff9800}.btn.btn-outline-warning.focus,.btn.btn-outline-warning:focus,.btn.btn-outline-warning:hover{color:#ff9800;background-color:hsla(0,0%,60%,.2);border-color:#ff9800}.btn.btn-outline-warning.active,.btn.btn-outline-warning:active,.open>.btn.btn-outline-warning.dropdown-toggle,.show>.btn.btn-outline-warning.dropdown-toggle{color:#ff9800;background-color:hsla(0,0%,60%,.2);border-color:#ff9800;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.btn.btn-outline-warning.active.focus,.btn.btn-outline-warning.active:focus,.btn.btn-outline-warning.active:hover,.btn.btn-outline-warning:active.focus,.btn.btn-outline-warning:active:focus,.btn.btn-outline-warning:active:hover,.open>.btn.btn-outline-warning.dropdown-toggle.focus,.open>.btn.btn-outline-warning.dropdown-toggle:focus,.open>.btn.btn-outline-warning.dropdown-toggle:hover,.show>.btn.btn-outline-warning.dropdown-toggle.focus,.show>.btn.btn-outline-warning.dropdown-toggle:focus,.show>.btn.btn-outline-warning.dropdown-toggle:hover{color:#ff9800;background-color:hsla(0,0%,60%,.4);border-color:#ff9800}.open>.btn.btn-outline-warning.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.btn.btn-outline-warning.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,60%,.2)}.bg-inverse .btn.btn-outline-warning,.btn.btn-outline-warning.disabled.focus,.btn.btn-outline-warning.disabled:focus,.btn.btn-outline-warning.disabled:hover,.btn.btn-outline-warning:disabled.focus,.btn.btn-outline-warning:disabled:focus,.btn.btn-outline-warning:disabled:hover{background-color:transparent;border-color:#ff9800}.bg-inverse .btn.btn-outline-warning{color:#ff9800}.bg-inverse .btn.btn-outline-warning.focus,.bg-inverse .btn.btn-outline-warning:focus,.bg-inverse .btn.btn-outline-warning:hover{color:#ff9800;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-warning.active,.bg-inverse .btn.btn-outline-warning:active,.open>.bg-inverse .btn.btn-outline-warning.dropdown-toggle,.show>.bg-inverse .btn.btn-outline-warning.dropdown-toggle{color:#ff9800;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.bg-inverse .btn.btn-outline-warning.active.focus,.bg-inverse .btn.btn-outline-warning.active:focus,.bg-inverse .btn.btn-outline-warning.active:hover,.bg-inverse .btn.btn-outline-warning:active.focus,.bg-inverse .btn.btn-outline-warning:active:focus,.bg-inverse .btn.btn-outline-warning:active:hover,.open>.bg-inverse .btn.btn-outline-warning.dropdown-toggle.focus,.open>.bg-inverse .btn.btn-outline-warning.dropdown-toggle:focus,.open>.bg-inverse .btn.btn-outline-warning.dropdown-toggle:hover,.show>.bg-inverse .btn.btn-outline-warning.dropdown-toggle.focus,.show>.bg-inverse .btn.btn-outline-warning.dropdown-toggle:focus,.show>.bg-inverse .btn.btn-outline-warning.dropdown-toggle:hover{color:#ff9800;background-color:hsla(0,0%,80%,.25);border-color:hsla(0,0%,80%,.25)}.open>.bg-inverse .btn.btn-outline-warning.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.bg-inverse .btn.btn-outline-warning.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-warning.disabled.focus,.bg-inverse .btn.btn-outline-warning.disabled:focus,.bg-inverse .btn.btn-outline-warning.disabled:hover,.bg-inverse .btn.btn-outline-warning:disabled.focus,.bg-inverse .btn.btn-outline-warning:disabled:focus,.bg-inverse .btn.btn-outline-warning:disabled:hover{background-color:transparent;border-color:#ff9800}.btn.btn-outline-warning.btn-link{background-color:transparent}.btn.btn-outline-danger{color:#f44336;background-color:transparent;border-color:#f44336}.btn.btn-outline-danger.focus,.btn.btn-outline-danger:focus,.btn.btn-outline-danger:hover{color:#f44336;background-color:hsla(0,0%,60%,.2);border-color:#f44336}.btn.btn-outline-danger.active,.btn.btn-outline-danger:active,.open>.btn.btn-outline-danger.dropdown-toggle,.show>.btn.btn-outline-danger.dropdown-toggle{color:#f44336;background-color:hsla(0,0%,60%,.2);border-color:#f44336;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.btn.btn-outline-danger.active.focus,.btn.btn-outline-danger.active:focus,.btn.btn-outline-danger.active:hover,.btn.btn-outline-danger:active.focus,.btn.btn-outline-danger:active:focus,.btn.btn-outline-danger:active:hover,.open>.btn.btn-outline-danger.dropdown-toggle.focus,.open>.btn.btn-outline-danger.dropdown-toggle:focus,.open>.btn.btn-outline-danger.dropdown-toggle:hover,.show>.btn.btn-outline-danger.dropdown-toggle.focus,.show>.btn.btn-outline-danger.dropdown-toggle:focus,.show>.btn.btn-outline-danger.dropdown-toggle:hover{color:#f44336;background-color:hsla(0,0%,60%,.4);border-color:#f44336}.open>.btn.btn-outline-danger.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.btn.btn-outline-danger.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,60%,.2)}.bg-inverse .btn.btn-outline-danger,.btn.btn-outline-danger.disabled.focus,.btn.btn-outline-danger.disabled:focus,.btn.btn-outline-danger.disabled:hover,.btn.btn-outline-danger:disabled.focus,.btn.btn-outline-danger:disabled:focus,.btn.btn-outline-danger:disabled:hover{background-color:transparent;border-color:#f44336}.bg-inverse .btn.btn-outline-danger{color:#f44336}.bg-inverse .btn.btn-outline-danger.focus,.bg-inverse .btn.btn-outline-danger:focus,.bg-inverse .btn.btn-outline-danger:hover{color:#f44336;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-danger.active,.bg-inverse .btn.btn-outline-danger:active,.open>.bg-inverse .btn.btn-outline-danger.dropdown-toggle,.show>.bg-inverse .btn.btn-outline-danger.dropdown-toggle{color:#f44336;background-color:hsla(0,0%,80%,.15);border-color:hsla(0,0%,80%,.15);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.bg-inverse .btn.btn-outline-danger.active.focus,.bg-inverse .btn.btn-outline-danger.active:focus,.bg-inverse .btn.btn-outline-danger.active:hover,.bg-inverse .btn.btn-outline-danger:active.focus,.bg-inverse .btn.btn-outline-danger:active:focus,.bg-inverse .btn.btn-outline-danger:active:hover,.open>.bg-inverse .btn.btn-outline-danger.dropdown-toggle.focus,.open>.bg-inverse .btn.btn-outline-danger.dropdown-toggle:focus,.open>.bg-inverse .btn.btn-outline-danger.dropdown-toggle:hover,.show>.bg-inverse .btn.btn-outline-danger.dropdown-toggle.focus,.show>.bg-inverse .btn.btn-outline-danger.dropdown-toggle:focus,.show>.bg-inverse .btn.btn-outline-danger.dropdown-toggle:hover{color:#f44336;background-color:hsla(0,0%,80%,.25);border-color:hsla(0,0%,80%,.25)}.open>.bg-inverse .btn.btn-outline-danger.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:transparent}.open>.bg-inverse .btn.btn-outline-danger.dropdown-toggle.bmd-btn-icon:hover{background-color:hsla(0,0%,80%,.15)}.bg-inverse .btn.btn-outline-danger.disabled.focus,.bg-inverse .btn.btn-outline-danger.disabled:focus,.bg-inverse .btn.btn-outline-danger.disabled:hover,.bg-inverse .btn.btn-outline-danger:disabled.focus,.bg-inverse .btn.btn-outline-danger:disabled:focus,.bg-inverse .btn.btn-outline-danger:disabled:hover{background-color:transparent;border-color:#f44336}.btn.btn-outline-danger.btn-link{background-color:transparent}.btn-group-lg .btn,.btn-group-lg>.btn,.btn.btn-lg{padding:1.125rem 2.25rem;font-size:.875rem;line-height:1.333333;border-radius:.2rem}.btn-group-sm .btn,.btn-group-sm>.btn,.btn.btn-sm{padding:.40625rem 1.25rem;font-size:.6875rem;line-height:1.5;border-radius:.2rem}.btn.btn-round{border-radius:30px}.btn.btn-fab,.btn.btn-just-icon{font-size:24px;height:41px;min-width:41px;width:41px;padding:0;overflow:hidden;position:relative;line-height:41px}.btn.btn-fab.btn-round,.btn.btn-just-icon.btn-round{border-radius:50%}.btn-group-sm .btn.btn-fab,.btn-group-sm .btn.btn-just-icon,.btn-group-sm>.btn.btn-fab,.btn-group-sm>.btn.btn-just-icon,.btn.btn-fab.btn-fab-mini,.btn.btn-fab.btn-sm,.btn.btn-just-icon.btn-fab-mini,.btn.btn-just-icon.btn-sm{height:30px;min-width:30px;width:30px}.btn-group-sm .btn.btn-fab .fa,.btn-group-sm .btn.btn-fab .material-icons,.btn-group-sm .btn.btn-just-icon .fa,.btn-group-sm .btn.btn-just-icon .material-icons,.btn-group-sm>.btn.btn-fab .fa,.btn-group-sm>.btn.btn-fab .material-icons,.btn-group-sm>.btn.btn-just-icon .fa,.btn-group-sm>.btn.btn-just-icon .material-icons,.btn.btn-fab.btn-fab-mini .fa,.btn.btn-fab.btn-fab-mini .material-icons,.btn.btn-fab.btn-sm .fa,.btn.btn-fab.btn-sm .material-icons,.btn.btn-just-icon.btn-fab-mini .fa,.btn.btn-just-icon.btn-fab-mini .material-icons,.btn.btn-just-icon.btn-sm .fa,.btn.btn-just-icon.btn-sm .material-icons{font-size:17px;line-height:29px}.btn-group-lg .btn.btn-fab,.btn-group-lg .btn.btn-just-icon,.btn-group-lg>.btn.btn-fab,.btn-group-lg>.btn.btn-just-icon,.btn.btn-fab.btn-lg,.btn.btn-just-icon.btn-lg{height:57px;min-width:57px;width:57px;line-height:56px}.btn-group-lg .btn.btn-fab .fa,.btn-group-lg .btn.btn-fab .material-icons,.btn-group-lg .btn.btn-just-icon .fa,.btn-group-lg .btn.btn-just-icon .material-icons,.btn-group-lg>.btn.btn-fab .fa,.btn-group-lg>.btn.btn-fab .material-icons,.btn-group-lg>.btn.btn-just-icon .fa,.btn-group-lg>.btn.btn-just-icon .material-icons,.btn.btn-fab.btn-lg .fa,.btn.btn-fab.btn-lg .material-icons,.btn.btn-just-icon.btn-lg .fa,.btn.btn-just-icon.btn-lg .material-icons{font-size:32px;line-height:56px}.btn.btn-fab .fa,.btn.btn-fab .material-icons,.btn.btn-just-icon .fa,.btn.btn-just-icon .material-icons{margin-top:0;position:absolute;width:100%;transform:none;left:0;top:0;height:100%;line-height:41px;font-size:20px}.btn-group-lg>.btn-just-icon.btn,.btn-just-icon.btn-lg{font-size:24px;height:41px;min-width:41px;width:41px}.input-group-btn>.btn{border:0}.btn .material-icons,.btn:not(.btn-just-icon):not(.btn-fab) .fa{position:relative;display:inline-block;top:0;margin-top:-1em;margin-bottom:-1em;font-size:1.1rem;vertical-align:middle}.bg-inverse .btn-group-vertical.disabled,.bg-inverse .btn-group-vertical:disabled,.bg-inverse .btn-group-vertical[disabled],.bg-inverse .btn-group.disabled,.bg-inverse .btn-group:disabled,.bg-inverse .btn-group[disabled],.bg-inverse .btn.disabled,.bg-inverse .btn:disabled,.bg-inverse .btn[disabled],.bg-inverse .input-group-btn .btn.disabled,.bg-inverse .input-group-btn .btn:disabled,.bg-inverse .input-group-btn .btn[disabled],.bg-inverse fieldset[disabled][disabled] .btn,.bg-inverse fieldset[disabled][disabled] .btn-group,.bg-inverse fieldset[disabled][disabled] .btn-group-vertical,.bg-inverse fieldset[disabled][disabled] .input-group-btn .btn{color:hsla(0,0%,100%,.3)}.btn-group,.btn-group-vertical{position:relative;margin:10px 1px}.btn-group-vertical .dropdown-menu,.btn-group .dropdown-menu{border-radius:0 0 .25rem .25rem}.btn-group-vertical.btn-group-raised,.btn-group.btn-group-raised{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.btn-group-vertical .btn,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn-group,.btn-group-vertical .btn:active,.btn-group-vertical>.btn-group,.btn-group .btn,.btn-group .btn+.btn,.btn-group .btn-group,.btn-group .btn:active,.btn-group>.btn-group{margin:0}.form-check{margin-bottom:.5rem}.form-check,.form-check .form-check-label{padding-left:0}.form-check .form-check-input{position:absolute;margin:0;z-index:-1;left:0;pointer-events:none}.form-check .form-check-sign:before{display:block;position:absolute;left:0;content:"";background-color:rgba(0,0,0,.84);height:20px;width:20px;border-radius:100%;z-index:1;opacity:0;margin:0;top:0;transform:scale3d(2.3,2.3,1)}.form-check .form-check-sign .check{position:relative;display:inline-block;width:20px;height:20px;border:1px solid rgba(0,0,0,.54);overflow:hidden;z-index:1;border-radius:3px}.form-check .form-check-sign .check:before{position:absolute;content:"";transform:rotate(45deg);display:block;margin-top:-3px;margin-left:7px;width:0;color:#fff;height:0;box-shadow:0 0 0 0,0 0 0 0,0 0 0 0,0 0 0 0,0 0 0 0,0 0 0 0,inset 0 0 0 0;animation:checkboxOff .3s forwards}.form-check .form-check-input:focus+.form-check-sign .check:after{opacity:.2}.form-check .form-check-input:checked~.form-check-sign .check{background:#9c27b0}.form-check .form-check-input:checked~.form-check-sign .check:before{color:#fff;box-shadow:0 0 0 10px,10px -10px 0 10px,32px 0 0 20px,0 32px 0 20px,-5px 5px 0 10px,20px -12px 0 11px;animation:b .3s forwards}.form-check .form-check-input:checked~.form-check-sign:before{animation:c .5s}.form-check .form-check-input:checked~.form-check-sign .check:after{animation:c .5s forwards}.form-check .form-check-input:not(:checked)+.form-check-sign .check:after,.form-check .form-check-input:not(:checked)+.form-check-sign:before{animation:c .5s}.form-check .rtl .form-check .form-check-sign .check:before{margin-right:10px}.form-check .form-check-input[disabled]+.circle,.form-check .form-check-input[disabled]~.form-check-sign .check,fieldset[disabled] .form-check,fieldset[disabled] .form-check .form-check-input{opacity:.5}.form-check .form-check-input[disabled]~.form-check-sign .check{border-color:#000;opacity:.26}.form-check .form-check-input[disabled]+.form-check-sign .check:after{background-color:rgba(0,0,0,.87);transform:rotate(-45deg)}.form-check .form-check-input[disabled][checked]+.form-check-sign .check{background-color:#000}.form-check .form-check-label{cursor:pointer;padding-left:25px;position:relative}.form-group.is-focused .form-check .form-check-label{color:rgba(0,0,0,.26)}.form-group.is-focused .form-check .form-check-label:focus,.form-group.is-focused .form-check .form-check-label:hover{color:rgba(0,0,0,.54)}fieldset[disabled] .form-group.is-focused .form-check .form-check-label{color:rgba(0,0,0,.26)}.form-check .form-check-label span{display:block;position:absolute;left:-1px;top:-1px;transition-duration:.2s}.form-check .form-check-label .circle{border:1px solid rgba(0,0,0,.54);height:15px;width:15px;border-radius:100%;top:1px}.form-check .form-check-label .circle .check{height:15px;width:15px;border-radius:100%;background-color:#9c27b0;transform:scale3d(0,0,0)}.form-check .form-check-input{opacity:0;height:0;width:0;overflow:hidden}.form-check .form-check-input:checked~.check,.form-check .form-check-input:checked~.circle{opacity:1}.form-check .form-check-input:checked~.check{background-color:#9c27b0}.form-check .form-check-input:checked~.circle{border-color:#9c27b0}.form-check .form-check-input:checked .check:before{animation:b .5s forwards}.form-check .form-check-input:checked~.circle .check{transform:scale3d(.65,.65,1)}.form-check .form-check-input[disabled]~.check,.form-check .form-check-input[disabled]~.circle{opacity:.26}.form-check .form-check-input[disabled]~.check{background-color:#000}.form-check .form-check-input[disabled]~.circle{border-color:#000}.form-check .form-check-input[disabled]+.circle .check{background-color:#000}.form-check .form-check-sign{vertical-align:middle;position:relative;top:-2px;float:left;padding-right:10px;display:inline-block}.form-check .form-check-label .circle:before{display:block;position:absolute;left:-1px;content:"";background-color:rgba(0,0,0,.84);height:15px;width:15px;border-radius:100%;z-index:1;opacity:0;margin:0;top:-1px;transform:scale3d(2.3,2.3,1)}.form-check .form-check-label .form-check-input:checked+.circle:before{animation:c .5s}.form-check .form-check-label .form-check-input:checked+.circle .check:before{color:#fff;box-shadow:0 0 0 10px,10px -10px 0 10px,32px 0 0 20px,0 32px 0 20px,-5px 5px 0 10px,20px -12px 0 11px;animation:b .3s forwards}.form-check+.form-check{margin-top:0}@keyframes b{0%{box-shadow:0 0 0 10px,10px -10px 0 10px,32px 0 0 20px,0 32px 0 20px,-5px 5px 0 10px,15px 2px 0 11px}50%{box-shadow:0 0 0 10px,10px -10px 0 10px,32px 0 0 20px,0 32px 0 20px,-5px 5px 0 10px,20px 2px 0 11px}to{box-shadow:0 0 0 10px,10px -10px 0 10px,32px 0 0 20px,0 32px 0 20px,-5px 5px 0 10px,20px -12px 0 11px}}@keyframes c{0%{opacity:0}50%{opacity:.2}to{opacity:0}}form{margin-bottom:1.125rem}.card form{margin:0}.navbar form{margin-bottom:0}.navbar form .bmd-form-group{display:inline-block;padding-top:0}.navbar form .btn{margin-bottom:0}.form-control{background:no-repeat bottom,50% calc(100% - 1px);background-size:0 100%,100% 100%;border:0;height:36px;transition:background 0s ease-out;padding-left:0;padding-right:0;border-radius:0;font-size:14px}.bmd-form-group.is-focused .form-control,.form-control:focus{background-size:100% 100%,100% 100%;transition-duration:.3s;box-shadow:none}.form-control::-moz-placeholder{color:#aaa;font-weight:400;font-size:14px}.form-control:-ms-input-placeholder{color:#aaa;font-weight:400;font-size:14px}.form-control::-webkit-input-placeholder{color:#aaa;font-weight:400;font-size:14px}.has-white .form-control::-moz-placeholder{color:#fff}.has-white .form-control:-ms-input-placeholder{color:#fff}.has-white .form-control::-webkit-input-placeholder{color:#fff}.bmd-help{position:absolute;display:none;font-size:.8rem;font-weight:400}.bmd-form-group.is-focused .bmd-help{display:block}.bmd-help:nth-of-type(2){padding-top:1rem}.bmd-help+.bmd-help{position:relative;margin-bottom:0}.checkbox-inline,.checkbox label,.is-focused .checkbox-inline,.is-focused .checkbox label,.is-focused .radio-inline,.is-focused .radio label,.is-focused .switch label,.radio-inline,.radio label,.switch label{color:#999}.checkbox-inline label:has(input[type=checkbox][disabled]),.checkbox-inline label:has(input[type=checkbox][disabled]):focus,.checkbox-inline label:has(input[type=checkbox][disabled]):hover,.checkbox-inline label:has(input[type=radio][disabled]),.checkbox-inline label:has(input[type=radio][disabled]):focus,.checkbox-inline label:has(input[type=radio][disabled]):hover,.checkbox label label:has(input[type=checkbox][disabled]),.checkbox label label:has(input[type=checkbox][disabled]):focus,.checkbox label label:has(input[type=checkbox][disabled]):hover,.checkbox label label:has(input[type=radio][disabled]),.checkbox label label:has(input[type=radio][disabled]):focus,.checkbox label label:has(input[type=radio][disabled]):hover,.is-focused .checkbox-inline label:has(input[type=checkbox][disabled]),.is-focused .checkbox-inline label:has(input[type=checkbox][disabled]):focus,.is-focused .checkbox-inline label:has(input[type=checkbox][disabled]):hover,.is-focused .checkbox-inline label:has(input[type=radio][disabled]),.is-focused .checkbox-inline label:has(input[type=radio][disabled]):focus,.is-focused .checkbox-inline label:has(input[type=radio][disabled]):hover,.is-focused .checkbox label label:has(input[type=checkbox][disabled]),.is-focused .checkbox label label:has(input[type=checkbox][disabled]):focus,.is-focused .checkbox label label:has(input[type=checkbox][disabled]):hover,.is-focused .checkbox label label:has(input[type=radio][disabled]),.is-focused .checkbox label label:has(input[type=radio][disabled]):focus,.is-focused .checkbox label label:has(input[type=radio][disabled]):hover,.is-focused .radio-inline label:has(input[type=checkbox][disabled]),.is-focused .radio-inline label:has(input[type=checkbox][disabled]):focus,.is-focused .radio-inline label:has(input[type=checkbox][disabled]):hover,.is-focused .radio-inline label:has(input[type=radio][disabled]),.is-focused .radio-inline label:has(input[type=radio][disabled]):focus,.is-focused .radio-inline label:has(input[type=radio][disabled]):hover,.is-focused .radio label label:has(input[type=checkbox][disabled]),.is-focused .radio label label:has(input[type=checkbox][disabled]):focus,.is-focused .radio label label:has(input[type=checkbox][disabled]):hover,.is-focused .radio label label:has(input[type=radio][disabled]),.is-focused .radio label label:has(input[type=radio][disabled]):focus,.is-focused .radio label label:has(input[type=radio][disabled]):hover,.is-focused .switch label label:has(input[type=checkbox][disabled]),.is-focused .switch label label:has(input[type=checkbox][disabled]):focus,.is-focused .switch label label:has(input[type=checkbox][disabled]):hover,.is-focused .switch label label:has(input[type=radio][disabled]),.is-focused .switch label label:has(input[type=radio][disabled]):focus,.is-focused .switch label label:has(input[type=radio][disabled]):hover,.radio-inline label:has(input[type=checkbox][disabled]),.radio-inline label:has(input[type=checkbox][disabled]):focus,.radio-inline label:has(input[type=checkbox][disabled]):hover,.radio-inline label:has(input[type=radio][disabled]),.radio-inline label:has(input[type=radio][disabled]):focus,.radio-inline label:has(input[type=radio][disabled]):hover,.radio label label:has(input[type=checkbox][disabled]),.radio label label:has(input[type=checkbox][disabled]):focus,.radio label label:has(input[type=checkbox][disabled]):hover,.radio label label:has(input[type=radio][disabled]),.radio label label:has(input[type=radio][disabled]):focus,.radio label label:has(input[type=radio][disabled]):hover,.switch label label:has(input[type=checkbox][disabled]),.switch label label:has(input[type=checkbox][disabled]):focus,.switch label label:has(input[type=checkbox][disabled]):hover,.switch label label:has(input[type=radio][disabled]),.switch label label:has(input[type=radio][disabled]):focus,.switch label label:has(input[type=radio][disabled]):hover,fieldset[disabled] .checkbox-inline,fieldset[disabled] .checkbox-inline:focus,fieldset[disabled] .checkbox-inline:hover,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox label:focus,fieldset[disabled] .checkbox label:hover,fieldset[disabled] .is-focused .checkbox-inline,fieldset[disabled] .is-focused .checkbox-inline:focus,fieldset[disabled] .is-focused .checkbox-inline:hover,fieldset[disabled] .is-focused .checkbox label,fieldset[disabled] .is-focused .checkbox label:focus,fieldset[disabled] .is-focused .checkbox label:hover,fieldset[disabled] .is-focused .radio-inline,fieldset[disabled] .is-focused .radio-inline:focus,fieldset[disabled] .is-focused .radio-inline:hover,fieldset[disabled] .is-focused .radio label,fieldset[disabled] .is-focused .radio label:focus,fieldset[disabled] .is-focused .radio label:hover,fieldset[disabled] .is-focused .switch label,fieldset[disabled] .is-focused .switch label:focus,fieldset[disabled] .is-focused .switch label:hover,fieldset[disabled] .radio-inline,fieldset[disabled] .radio-inline:focus,fieldset[disabled] .radio-inline:hover,fieldset[disabled] .radio label,fieldset[disabled] .radio label:focus,fieldset[disabled] .radio label:hover,fieldset[disabled] .switch label,fieldset[disabled] .switch label:focus,fieldset[disabled] .switch label:hover{color:#999}[class*=" bmd-label"],[class^=bmd-label]{color:#999}.form-control,.is-focused .form-control{background-image:linear-gradient(0deg,#9c27b0 2px,rgba(156,39,176,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.form-control:invalid{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.form-control:read-only{background-image:linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.form-control.disabled,.form-control:disabled,.form-control[disabled],fieldset[disabled][disabled] .form-control{background-image:linear-gradient(90deg,#d2d2d2 0,#d2d2d2 30%,transparent 0,transparent);background-repeat:repeat-x;background-size:3px 1px}.form-control.form-control-success,.is-focused .form-control.form-control-success{background-image:linear-gradient(0deg,#9c27b0 2px,rgba(156,39,176,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg=="}.form-control.form-control-warning,.is-focused .form-control.form-control-warning{background-image:linear-gradient(0deg,#9c27b0 2px,rgba(156,39,176,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+"}.form-control.form-control-danger,.is-focused .form-control.form-control-danger{background-image:linear-gradient(0deg,#9c27b0 2px,rgba(156,39,176,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZDk1MzRmIiBkPSJNNDQ3IDU0NC40Yy0xNC40IDE0LjQtMzcuNiAxNC40LTUyIDBsLTg5LTkyLjctODkgOTIuN2MtMTQuNSAxNC40LTM3LjcgMTQuNC01MiAwLTE0LjQtMTQuNC0xNC40LTM3LjYgMC01Mmw5Mi40LTk2LjMtOTIuNC05Ni4zYy0xNC40LTE0LjQtMTQuNC0zNy42IDAtNTJzMzcuNi0xNC4zIDUyIDBsODkgOTIuOCA4OS4yLTkyLjdjMTQuNC0xNC40IDM3LjYtMTQuNCA1MiAwIDE0LjMgMTQuNCAxNC4zIDM3LjYgMCA1MkwzNTQuNiAzOTZsOTIuNCA5Ni40YzE0LjQgMTQuNCAxNC40IDM3LjYgMCA1MnoiLz48L3N2Zz4="}.is-focused .valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#999}.is-focused .valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:hsla(0,0%,60%,.8);border-radius:.2rem}.is-focused .custom-select.is-valid,.is-focused .form-control.is-valid,.was-validated .is-focused .custom-select:valid,.was-validated .is-focused .form-control:valid{border-color:#999}.is-focused .custom-select.is-valid:focus,.is-focused .form-control.is-valid:focus,.was-validated .is-focused .custom-select:valid:focus,.was-validated .is-focused .form-control:valid:focus{border-color:#999;box-shadow:0 0 0 .2rem hsla(0,0%,60%,.25)}.is-focused .custom-select.is-valid~.valid-feedback,.is-focused .custom-select.is-valid~.valid-tooltip,.is-focused .form-control.is-valid~.valid-feedback,.is-focused .form-control.is-valid~.valid-tooltip,.was-validated .is-focused .custom-select:valid~.valid-feedback,.was-validated .is-focused .custom-select:valid~.valid-tooltip,.was-validated .is-focused .form-control:valid~.valid-feedback,.was-validated .is-focused .form-control:valid~.valid-tooltip{display:block}.is-focused .form-check-input.is-valid~.form-check-label,.was-validated .is-focused .form-check-input:valid~.form-check-label{color:#999}.is-focused .form-check-input.is-valid~.valid-feedback,.is-focused .form-check-input.is-valid~.valid-tooltip,.was-validated .is-focused .form-check-input:valid~.valid-feedback,.was-validated .is-focused .form-check-input:valid~.valid-tooltip{display:block}.is-focused .custom-control-input.is-valid~.custom-control-label,.was-validated .is-focused .custom-control-input:valid~.custom-control-label{color:#999}.is-focused .custom-control-input.is-valid~.custom-control-label:before,.was-validated .is-focused .custom-control-input:valid~.custom-control-label:before{background-color:#d9d9d9}.is-focused .custom-control-input.is-valid~.valid-feedback,.is-focused .custom-control-input.is-valid~.valid-tooltip,.was-validated .is-focused .custom-control-input:valid~.valid-feedback,.was-validated .is-focused .custom-control-input:valid~.valid-tooltip{display:block}.is-focused .custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .is-focused .custom-control-input:valid:checked~.custom-control-label:before{background-color:#b3b3b3}.is-focused .custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .is-focused .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem hsla(0,0%,60%,.25)}.is-focused .custom-file-input.is-valid~.custom-file-label,.was-validated .is-focused .custom-file-input:valid~.custom-file-label{border-color:#999}.is-focused .custom-file-input.is-valid~.custom-file-label:before,.was-validated .is-focused .custom-file-input:valid~.custom-file-label:before{border-color:inherit}.is-focused .custom-file-input.is-valid~.valid-feedback,.is-focused .custom-file-input.is-valid~.valid-tooltip,.was-validated .is-focused .custom-file-input:valid~.valid-feedback,.was-validated .is-focused .custom-file-input:valid~.valid-tooltip{display:block}.is-focused .custom-file-input.is-valid:focus~.custom-file-label,.was-validated .is-focused .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem hsla(0,0%,60%,.25)}.is-focused [class*=" bmd-label"],.is-focused [class^=bmd-label]{color:#9c27b0}.is-focused .bmd-label-placeholder{color:#999}.is-focused .form-control{border-color:#d2d2d2}.is-focused .bmd-help{color:#555}.has-success [class*=" bmd-label"],.has-success [class^=bmd-label]{color:#4caf50}.has-success .form-control,.is-focused .has-success .form-control{background-image:linear-gradient(0deg,#4caf50 2px,rgba(76,175,80,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-success .form-control:invalid{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-success .form-control:read-only{background-image:linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-success .form-control.disabled,.has-success .form-control:disabled,.has-success .form-control[disabled],fieldset[disabled][disabled] .has-success .form-control{background-image:linear-gradient(90deg,#d2d2d2 0,#d2d2d2 30%,transparent 0,transparent);background-repeat:repeat-x;background-size:3px 1px}.has-success .form-control.form-control-success,.is-focused .has-success .form-control.form-control-success{background-image:linear-gradient(0deg,#4caf50 2px,rgba(76,175,80,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg=="}.has-success .form-control.form-control-warning,.is-focused .has-success .form-control.form-control-warning{background-image:linear-gradient(0deg,#4caf50 2px,rgba(76,175,80,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+"}.has-success .form-control.form-control-danger,.is-focused .has-success .form-control.form-control-danger{background-image:linear-gradient(0deg,#4caf50 2px,rgba(76,175,80,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZDk1MzRmIiBkPSJNNDQ3IDU0NC40Yy0xNC40IDE0LjQtMzcuNiAxNC40LTUyIDBsLTg5LTkyLjctODkgOTIuN2MtMTQuNSAxNC40LTM3LjcgMTQuNC01MiAwLTE0LjQtMTQuNC0xNC40LTM3LjYgMC01Mmw5Mi40LTk2LjMtOTIuNC05Ni4zYy0xNC40LTE0LjQtMTQuNC0zNy42IDAtNTJzMzcuNi0xNC4zIDUyIDBsODkgOTIuOCA4OS4yLTkyLjdjMTQuNC0xNC40IDM3LjYtMTQuNCA1MiAwIDE0LjMgMTQuNCAxNC4zIDM3LjYgMCA1MkwzNTQuNiAzOTZsOTIuNCA5Ni40YzE0LjQgMTQuNCAxNC40IDM3LjYgMCA1MnoiLz48L3N2Zz4="}.has-success .is-focused .valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#4caf50}.has-success .is-focused .valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(76,175,80,.8);border-radius:.2rem}.has-success .is-focused .custom-select.is-valid,.has-success .is-focused .form-control.is-valid,.was-validated .has-success .is-focused .custom-select:valid,.was-validated .has-success .is-focused .form-control:valid{border-color:#4caf50}.has-success .is-focused .custom-select.is-valid:focus,.has-success .is-focused .form-control.is-valid:focus,.was-validated .has-success .is-focused .custom-select:valid:focus,.was-validated .has-success .is-focused .form-control:valid:focus{border-color:#4caf50;box-shadow:0 0 0 .2rem rgba(76,175,80,.25)}.has-success .is-focused .custom-select.is-valid~.valid-feedback,.has-success .is-focused .custom-select.is-valid~.valid-tooltip,.has-success .is-focused .form-control.is-valid~.valid-feedback,.has-success .is-focused .form-control.is-valid~.valid-tooltip,.was-validated .has-success .is-focused .custom-select:valid~.valid-feedback,.was-validated .has-success .is-focused .custom-select:valid~.valid-tooltip,.was-validated .has-success .is-focused .form-control:valid~.valid-feedback,.was-validated .has-success .is-focused .form-control:valid~.valid-tooltip{display:block}.has-success .is-focused .form-check-input.is-valid~.form-check-label,.was-validated .has-success .is-focused .form-check-input:valid~.form-check-label{color:#4caf50}.has-success .is-focused .form-check-input.is-valid~.valid-feedback,.has-success .is-focused .form-check-input.is-valid~.valid-tooltip,.was-validated .has-success .is-focused .form-check-input:valid~.valid-feedback,.was-validated .has-success .is-focused .form-check-input:valid~.valid-tooltip{display:block}.has-success .is-focused .custom-control-input.is-valid~.custom-control-label,.was-validated .has-success .is-focused .custom-control-input:valid~.custom-control-label{color:#4caf50}.has-success .is-focused .custom-control-input.is-valid~.custom-control-label:before,.was-validated .has-success .is-focused .custom-control-input:valid~.custom-control-label:before{background-color:#a3d7a5}.has-success .is-focused .custom-control-input.is-valid~.valid-feedback,.has-success .is-focused .custom-control-input.is-valid~.valid-tooltip,.was-validated .has-success .is-focused .custom-control-input:valid~.valid-feedback,.was-validated .has-success .is-focused .custom-control-input:valid~.valid-tooltip{display:block}.has-success .is-focused .custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .has-success .is-focused .custom-control-input:valid:checked~.custom-control-label:before{background-color:#6ec071}.has-success .is-focused .custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .has-success .is-focused .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem rgba(76,175,80,.25)}.has-success .is-focused .custom-file-input.is-valid~.custom-file-label,.was-validated .has-success .is-focused .custom-file-input:valid~.custom-file-label{border-color:#4caf50}.has-success .is-focused .custom-file-input.is-valid~.custom-file-label:before,.was-validated .has-success .is-focused .custom-file-input:valid~.custom-file-label:before{border-color:inherit}.has-success .is-focused .custom-file-input.is-valid~.valid-feedback,.has-success .is-focused .custom-file-input.is-valid~.valid-tooltip,.was-validated .has-success .is-focused .custom-file-input:valid~.valid-feedback,.was-validated .has-success .is-focused .custom-file-input:valid~.valid-tooltip{display:block}.has-success .is-focused .custom-file-input.is-valid:focus~.custom-file-label,.was-validated .has-success .is-focused .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(76,175,80,.25)}.has-success .is-focused .bmd-label-placeholder,.has-success .is-focused [class*=" bmd-label"],.has-success .is-focused [class^=bmd-label]{color:#4caf50}.has-success .is-focused .form-control{border-color:#4caf50}.has-success .is-focused .bmd-help{color:#555}.has-info [class*=" bmd-label"],.has-info [class^=bmd-label]{color:#00bcd4}.has-info .form-control,.is-focused .has-info .form-control{background-image:linear-gradient(0deg,#00bcd4 2px,rgba(0,188,212,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-info .form-control:invalid{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-info .form-control:read-only{background-image:linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-info .form-control.disabled,.has-info .form-control:disabled,.has-info .form-control[disabled],fieldset[disabled][disabled] .has-info .form-control{background-image:linear-gradient(90deg,#d2d2d2 0,#d2d2d2 30%,transparent 0,transparent);background-repeat:repeat-x;background-size:3px 1px}.has-info .form-control.form-control-success,.is-focused .has-info .form-control.form-control-success{background-image:linear-gradient(0deg,#00bcd4 2px,rgba(0,188,212,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg=="}.has-info .form-control.form-control-warning,.is-focused .has-info .form-control.form-control-warning{background-image:linear-gradient(0deg,#00bcd4 2px,rgba(0,188,212,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+"}.has-info .form-control.form-control-danger,.is-focused .has-info .form-control.form-control-danger{background-image:linear-gradient(0deg,#00bcd4 2px,rgba(0,188,212,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZDk1MzRmIiBkPSJNNDQ3IDU0NC40Yy0xNC40IDE0LjQtMzcuNiAxNC40LTUyIDBsLTg5LTkyLjctODkgOTIuN2MtMTQuNSAxNC40LTM3LjcgMTQuNC01MiAwLTE0LjQtMTQuNC0xNC40LTM3LjYgMC01Mmw5Mi40LTk2LjMtOTIuNC05Ni4zYy0xNC40LTE0LjQtMTQuNC0zNy42IDAtNTJzMzcuNi0xNC4zIDUyIDBsODkgOTIuOCA4OS4yLTkyLjdjMTQuNC0xNC40IDM3LjYtMTQuNCA1MiAwIDE0LjMgMTQuNCAxNC4zIDM3LjYgMCA1MkwzNTQuNiAzOTZsOTIuNCA5Ni40YzE0LjQgMTQuNCAxNC40IDM3LjYgMCA1MnoiLz48L3N2Zz4="}.has-info .is-focused .valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#00bcd4}.has-info .is-focused .valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(0,188,212,.8);border-radius:.2rem}.has-info .is-focused .custom-select.is-valid,.has-info .is-focused .form-control.is-valid,.was-validated .has-info .is-focused .custom-select:valid,.was-validated .has-info .is-focused .form-control:valid{border-color:#00bcd4}.has-info .is-focused .custom-select.is-valid:focus,.has-info .is-focused .form-control.is-valid:focus,.was-validated .has-info .is-focused .custom-select:valid:focus,.was-validated .has-info .is-focused .form-control:valid:focus{border-color:#00bcd4;box-shadow:0 0 0 .2rem rgba(0,188,212,.25)}.has-info .is-focused .custom-select.is-valid~.valid-feedback,.has-info .is-focused .custom-select.is-valid~.valid-tooltip,.has-info .is-focused .form-control.is-valid~.valid-feedback,.has-info .is-focused .form-control.is-valid~.valid-tooltip,.was-validated .has-info .is-focused .custom-select:valid~.valid-feedback,.was-validated .has-info .is-focused .custom-select:valid~.valid-tooltip,.was-validated .has-info .is-focused .form-control:valid~.valid-feedback,.was-validated .has-info .is-focused .form-control:valid~.valid-tooltip{display:block}.has-info .is-focused .form-check-input.is-valid~.form-check-label,.was-validated .has-info .is-focused .form-check-input:valid~.form-check-label{color:#00bcd4}.has-info .is-focused .form-check-input.is-valid~.valid-feedback,.has-info .is-focused .form-check-input.is-valid~.valid-tooltip,.was-validated .has-info .is-focused .form-check-input:valid~.valid-feedback,.was-validated .has-info .is-focused .form-check-input:valid~.valid-tooltip{display:block}.has-info .is-focused .custom-control-input.is-valid~.custom-control-label,.was-validated .has-info .is-focused .custom-control-input:valid~.custom-control-label{color:#00bcd4}.has-info .is-focused .custom-control-input.is-valid~.custom-control-label:before,.was-validated .has-info .is-focused .custom-control-input:valid~.custom-control-label:before{background-color:#55ecff}.has-info .is-focused .custom-control-input.is-valid~.valid-feedback,.has-info .is-focused .custom-control-input.is-valid~.valid-tooltip,.was-validated .has-info .is-focused .custom-control-input:valid~.valid-feedback,.was-validated .has-info .is-focused .custom-control-input:valid~.valid-tooltip{display:block}.has-info .is-focused .custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .has-info .is-focused .custom-control-input:valid:checked~.custom-control-label:before{background-color:#08e3ff}.has-info .is-focused .custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .has-info .is-focused .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem rgba(0,188,212,.25)}.has-info .is-focused .custom-file-input.is-valid~.custom-file-label,.was-validated .has-info .is-focused .custom-file-input:valid~.custom-file-label{border-color:#00bcd4}.has-info .is-focused .custom-file-input.is-valid~.custom-file-label:before,.was-validated .has-info .is-focused .custom-file-input:valid~.custom-file-label:before{border-color:inherit}.has-info .is-focused .custom-file-input.is-valid~.valid-feedback,.has-info .is-focused .custom-file-input.is-valid~.valid-tooltip,.was-validated .has-info .is-focused .custom-file-input:valid~.valid-feedback,.was-validated .has-info .is-focused .custom-file-input:valid~.valid-tooltip{display:block}.has-info .is-focused .custom-file-input.is-valid:focus~.custom-file-label,.was-validated .has-info .is-focused .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(0,188,212,.25)}.has-info .is-focused .bmd-label-placeholder,.has-info .is-focused [class*=" bmd-label"],.has-info .is-focused [class^=bmd-label]{color:#00bcd4}.has-info .is-focused .form-control{border-color:#00bcd4}.has-info .is-focused .bmd-help{color:#555}.has-white [class*=" bmd-label"],.has-white [class^=bmd-label]{color:#fff}.has-white .form-control,.is-focused .has-white .form-control{background-image:linear-gradient(0deg,#fff 2px,hsla(0,0%,100%,0) 0),linear-gradient(0deg,#fff 1px,hsla(0,0%,100%,0) 0)}.has-white .form-control:invalid{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#fff 1px,hsla(0,0%,100%,0) 0)}.has-white .form-control:read-only{background-image:linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),linear-gradient(0deg,#fff 1px,hsla(0,0%,100%,0) 0)}.has-white .form-control.disabled,.has-white .form-control:disabled,.has-white .form-control[disabled],fieldset[disabled][disabled] .has-white .form-control{background-image:linear-gradient(90deg,#fff 0,#fff 30%,transparent 0,transparent);background-repeat:repeat-x;background-size:3px 1px}.has-white .form-control.form-control-success,.is-focused .has-white .form-control.form-control-success{background-image:linear-gradient(0deg,#fff 2px,hsla(0,0%,100%,0) 0),linear-gradient(0deg,#fff 1px,hsla(0,0%,100%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg=="}.has-white .form-control.form-control-warning,.is-focused .has-white .form-control.form-control-warning{background-image:linear-gradient(0deg,#fff 2px,hsla(0,0%,100%,0) 0),linear-gradient(0deg,#fff 1px,hsla(0,0%,100%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+"}.has-white .form-control.form-control-danger,.is-focused .has-white .form-control.form-control-danger{background-image:linear-gradient(0deg,#fff 2px,hsla(0,0%,100%,0) 0),linear-gradient(0deg,#fff 1px,hsla(0,0%,100%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZDk1MzRmIiBkPSJNNDQ3IDU0NC40Yy0xNC40IDE0LjQtMzcuNiAxNC40LTUyIDBsLTg5LTkyLjctODkgOTIuN2MtMTQuNSAxNC40LTM3LjcgMTQuNC01MiAwLTE0LjQtMTQuNC0xNC40LTM3LjYgMC01Mmw5Mi40LTk2LjMtOTIuNC05Ni4zYy0xNC40LTE0LjQtMTQuNC0zNy42IDAtNTJzMzcuNi0xNC4zIDUyIDBsODkgOTIuOCA4OS4yLTkyLjdjMTQuNC0xNC40IDM3LjYtMTQuNCA1MiAwIDE0LjMgMTQuNCAxNC4zIDM3LjYgMCA1MkwzNTQuNiAzOTZsOTIuNCA5Ni40YzE0LjQgMTQuNCAxNC40IDM3LjYgMCA1MnoiLz48L3N2Zz4="}.has-white .is-focused .valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#fff}.has-white .is-focused .valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:hsla(0,0%,100%,.8);border-radius:.2rem}.has-white .is-focused .custom-select.is-valid,.has-white .is-focused .form-control.is-valid,.was-validated .has-white .is-focused .custom-select:valid,.was-validated .has-white .is-focused .form-control:valid{border-color:#fff}.has-white .is-focused .custom-select.is-valid:focus,.has-white .is-focused .form-control.is-valid:focus,.was-validated .has-white .is-focused .custom-select:valid:focus,.was-validated .has-white .is-focused .form-control:valid:focus{border-color:#fff;box-shadow:0 0 0 .2rem hsla(0,0%,100%,.25)}.has-white .is-focused .custom-select.is-valid~.valid-feedback,.has-white .is-focused .custom-select.is-valid~.valid-tooltip,.has-white .is-focused .form-control.is-valid~.valid-feedback,.has-white .is-focused .form-control.is-valid~.valid-tooltip,.was-validated .has-white .is-focused .custom-select:valid~.valid-feedback,.was-validated .has-white .is-focused .custom-select:valid~.valid-tooltip,.was-validated .has-white .is-focused .form-control:valid~.valid-feedback,.was-validated .has-white .is-focused .form-control:valid~.valid-tooltip{display:block}.has-white .is-focused .form-check-input.is-valid~.form-check-label,.was-validated .has-white .is-focused .form-check-input:valid~.form-check-label{color:#fff}.has-white .is-focused .form-check-input.is-valid~.valid-feedback,.has-white .is-focused .form-check-input.is-valid~.valid-tooltip,.was-validated .has-white .is-focused .form-check-input:valid~.valid-feedback,.was-validated .has-white .is-focused .form-check-input:valid~.valid-tooltip{display:block}.has-white .is-focused .custom-control-input.is-valid~.custom-control-label,.was-validated .has-white .is-focused .custom-control-input:valid~.custom-control-label{color:#fff}.has-white .is-focused .custom-control-input.is-valid~.custom-control-label:before,.was-validated .has-white .is-focused .custom-control-input:valid~.custom-control-label:before{background-color:#fff}.has-white .is-focused .custom-control-input.is-valid~.valid-feedback,.has-white .is-focused .custom-control-input.is-valid~.valid-tooltip,.was-validated .has-white .is-focused .custom-control-input:valid~.valid-feedback,.was-validated .has-white .is-focused .custom-control-input:valid~.valid-tooltip{display:block}.has-white .is-focused .custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .has-white .is-focused .custom-control-input:valid:checked~.custom-control-label:before{background-color:#fff}.has-white .is-focused .custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .has-white .is-focused .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem hsla(0,0%,100%,.25)}.has-white .is-focused .custom-file-input.is-valid~.custom-file-label,.was-validated .has-white .is-focused .custom-file-input:valid~.custom-file-label{border-color:#fff}.has-white .is-focused .custom-file-input.is-valid~.custom-file-label:before,.was-validated .has-white .is-focused .custom-file-input:valid~.custom-file-label:before{border-color:inherit}.has-white .is-focused .custom-file-input.is-valid~.valid-feedback,.has-white .is-focused .custom-file-input.is-valid~.valid-tooltip,.was-validated .has-white .is-focused .custom-file-input:valid~.valid-feedback,.was-validated .has-white .is-focused .custom-file-input:valid~.valid-tooltip{display:block}.has-white .is-focused .custom-file-input.is-valid:focus~.custom-file-label,.was-validated .has-white .is-focused .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem hsla(0,0%,100%,.25)}.has-white .is-focused .bmd-label-placeholder,.has-white .is-focused [class*=" bmd-label"],.has-white .is-focused [class^=bmd-label]{color:#fff}.has-white .is-focused .form-control{border-color:#fff}.has-white .is-focused .bmd-help{color:#555}.has-white .form-control:focus{color:#fff}.has-warning [class*=" bmd-label"],.has-warning [class^=bmd-label]{color:#ff9800}.has-warning .form-control,.is-focused .has-warning .form-control{background-image:linear-gradient(0deg,#ff9800 2px,rgba(255,152,0,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-warning .form-control:invalid{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-warning .form-control:read-only{background-image:linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-warning .form-control.disabled,.has-warning .form-control:disabled,.has-warning .form-control[disabled],fieldset[disabled][disabled] .has-warning .form-control{background-image:linear-gradient(90deg,#d2d2d2 0,#d2d2d2 30%,transparent 0,transparent);background-repeat:repeat-x;background-size:3px 1px}.has-warning .form-control.form-control-success,.is-focused .has-warning .form-control.form-control-success{background-image:linear-gradient(0deg,#ff9800 2px,rgba(255,152,0,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg=="}.has-warning .form-control.form-control-warning,.is-focused .has-warning .form-control.form-control-warning{background-image:linear-gradient(0deg,#ff9800 2px,rgba(255,152,0,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+"}.has-warning .form-control.form-control-danger,.is-focused .has-warning .form-control.form-control-danger{background-image:linear-gradient(0deg,#ff9800 2px,rgba(255,152,0,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZDk1MzRmIiBkPSJNNDQ3IDU0NC40Yy0xNC40IDE0LjQtMzcuNiAxNC40LTUyIDBsLTg5LTkyLjctODkgOTIuN2MtMTQuNSAxNC40LTM3LjcgMTQuNC01MiAwLTE0LjQtMTQuNC0xNC40LTM3LjYgMC01Mmw5Mi40LTk2LjMtOTIuNC05Ni4zYy0xNC40LTE0LjQtMTQuNC0zNy42IDAtNTJzMzcuNi0xNC4zIDUyIDBsODkgOTIuOCA4OS4yLTkyLjdjMTQuNC0xNC40IDM3LjYtMTQuNCA1MiAwIDE0LjMgMTQuNCAxNC4zIDM3LjYgMCA1MkwzNTQuNiAzOTZsOTIuNCA5Ni40YzE0LjQgMTQuNCAxNC40IDM3LjYgMCA1MnoiLz48L3N2Zz4="}.has-warning .is-focused .valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#ff9800}.has-warning .is-focused .valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(255,152,0,.8);border-radius:.2rem}.has-warning .is-focused .custom-select.is-valid,.has-warning .is-focused .form-control.is-valid,.was-validated .has-warning .is-focused .custom-select:valid,.was-validated .has-warning .is-focused .form-control:valid{border-color:#ff9800}.has-warning .is-focused .custom-select.is-valid:focus,.has-warning .is-focused .form-control.is-valid:focus,.was-validated .has-warning .is-focused .custom-select:valid:focus,.was-validated .has-warning .is-focused .form-control:valid:focus{border-color:#ff9800;box-shadow:0 0 0 .2rem rgba(255,152,0,.25)}.has-warning .is-focused .custom-select.is-valid~.valid-feedback,.has-warning .is-focused .custom-select.is-valid~.valid-tooltip,.has-warning .is-focused .form-control.is-valid~.valid-feedback,.has-warning .is-focused .form-control.is-valid~.valid-tooltip,.was-validated .has-warning .is-focused .custom-select:valid~.valid-feedback,.was-validated .has-warning .is-focused .custom-select:valid~.valid-tooltip,.was-validated .has-warning .is-focused .form-control:valid~.valid-feedback,.was-validated .has-warning .is-focused .form-control:valid~.valid-tooltip{display:block}.has-warning .is-focused .form-check-input.is-valid~.form-check-label,.was-validated .has-warning .is-focused .form-check-input:valid~.form-check-label{color:#ff9800}.has-warning .is-focused .form-check-input.is-valid~.valid-feedback,.has-warning .is-focused .form-check-input.is-valid~.valid-tooltip,.was-validated .has-warning .is-focused .form-check-input:valid~.valid-feedback,.was-validated .has-warning .is-focused .form-check-input:valid~.valid-tooltip{display:block}.has-warning .is-focused .custom-control-input.is-valid~.custom-control-label,.was-validated .has-warning .is-focused .custom-control-input:valid~.custom-control-label{color:#ff9800}.has-warning .is-focused .custom-control-input.is-valid~.custom-control-label:before,.was-validated .has-warning .is-focused .custom-control-input:valid~.custom-control-label:before{background-color:#ffcc80}.has-warning .is-focused .custom-control-input.is-valid~.valid-feedback,.has-warning .is-focused .custom-control-input.is-valid~.valid-tooltip,.was-validated .has-warning .is-focused .custom-control-input:valid~.valid-feedback,.was-validated .has-warning .is-focused .custom-control-input:valid~.valid-tooltip{display:block}.has-warning .is-focused .custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .has-warning .is-focused .custom-control-input:valid:checked~.custom-control-label:before{background-color:#ffad33}.has-warning .is-focused .custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .has-warning .is-focused .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem rgba(255,152,0,.25)}.has-warning .is-focused .custom-file-input.is-valid~.custom-file-label,.was-validated .has-warning .is-focused .custom-file-input:valid~.custom-file-label{border-color:#ff9800}.has-warning .is-focused .custom-file-input.is-valid~.custom-file-label:before,.was-validated .has-warning .is-focused .custom-file-input:valid~.custom-file-label:before{border-color:inherit}.has-warning .is-focused .custom-file-input.is-valid~.valid-feedback,.has-warning .is-focused .custom-file-input.is-valid~.valid-tooltip,.was-validated .has-warning .is-focused .custom-file-input:valid~.valid-feedback,.was-validated .has-warning .is-focused .custom-file-input:valid~.valid-tooltip{display:block}.has-warning .is-focused .custom-file-input.is-valid:focus~.custom-file-label,.was-validated .has-warning .is-focused .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(255,152,0,.25)}.has-warning .is-focused .bmd-label-placeholder,.has-warning .is-focused [class*=" bmd-label"],.has-warning .is-focused [class^=bmd-label]{color:#ff9800}.has-warning .is-focused .form-control{border-color:#ff9800}.has-warning .is-focused .bmd-help{color:#555}.has-danger [class*=" bmd-label"],.has-danger [class^=bmd-label]{color:#f44336}.has-danger .form-control,.has-danger .form-control:invalid,.is-focused .has-danger .form-control{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-danger .form-control:read-only{background-image:linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-danger .form-control.disabled,.has-danger .form-control:disabled,.has-danger .form-control[disabled],fieldset[disabled][disabled] .has-danger .form-control{background-image:linear-gradient(90deg,#d2d2d2 0,#d2d2d2 30%,transparent 0,transparent);background-repeat:repeat-x;background-size:3px 1px}.has-danger .form-control.form-control-success,.is-focused .has-danger .form-control.form-control-success{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg=="}.has-danger .form-control.form-control-warning,.is-focused .has-danger .form-control.form-control-warning{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+"}.has-danger .form-control.form-control-danger,.is-focused .has-danger .form-control.form-control-danger{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZDk1MzRmIiBkPSJNNDQ3IDU0NC40Yy0xNC40IDE0LjQtMzcuNiAxNC40LTUyIDBsLTg5LTkyLjctODkgOTIuN2MtMTQuNSAxNC40LTM3LjcgMTQuNC01MiAwLTE0LjQtMTQuNC0xNC40LTM3LjYgMC01Mmw5Mi40LTk2LjMtOTIuNC05Ni4zYy0xNC40LTE0LjQtMTQuNC0zNy42IDAtNTJzMzcuNi0xNC4zIDUyIDBsODkgOTIuOCA4OS4yLTkyLjdjMTQuNC0xNC40IDM3LjYtMTQuNCA1MiAwIDE0LjMgMTQuNCAxNC4zIDM3LjYgMCA1MkwzNTQuNiAzOTZsOTIuNCA5Ni40YzE0LjQgMTQuNCAxNC40IDM3LjYgMCA1MnoiLz48L3N2Zz4="}.has-danger .is-focused .valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#f44336}.has-danger .is-focused .valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(244,67,54,.8);border-radius:.2rem}.has-danger .is-focused .custom-select.is-valid,.has-danger .is-focused .form-control.is-valid,.was-validated .has-danger .is-focused .custom-select:valid,.was-validated .has-danger .is-focused .form-control:valid{border-color:#f44336}.has-danger .is-focused .custom-select.is-valid:focus,.has-danger .is-focused .form-control.is-valid:focus,.was-validated .has-danger .is-focused .custom-select:valid:focus,.was-validated .has-danger .is-focused .form-control:valid:focus{border-color:#f44336;box-shadow:0 0 0 .2rem rgba(244,67,54,.25)}.has-danger .is-focused .custom-select.is-valid~.valid-feedback,.has-danger .is-focused .custom-select.is-valid~.valid-tooltip,.has-danger .is-focused .form-control.is-valid~.valid-feedback,.has-danger .is-focused .form-control.is-valid~.valid-tooltip,.was-validated .has-danger .is-focused .custom-select:valid~.valid-feedback,.was-validated .has-danger .is-focused .custom-select:valid~.valid-tooltip,.was-validated .has-danger .is-focused .form-control:valid~.valid-feedback,.was-validated .has-danger .is-focused .form-control:valid~.valid-tooltip{display:block}.has-danger .is-focused .form-check-input.is-valid~.form-check-label,.was-validated .has-danger .is-focused .form-check-input:valid~.form-check-label{color:#f44336}.has-danger .is-focused .form-check-input.is-valid~.valid-feedback,.has-danger .is-focused .form-check-input.is-valid~.valid-tooltip,.was-validated .has-danger .is-focused .form-check-input:valid~.valid-feedback,.was-validated .has-danger .is-focused .form-check-input:valid~.valid-tooltip{display:block}.has-danger .is-focused .custom-control-input.is-valid~.custom-control-label,.was-validated .has-danger .is-focused .custom-control-input:valid~.custom-control-label{color:#f44336}.has-danger .is-focused .custom-control-input.is-valid~.custom-control-label:before,.was-validated .has-danger .is-focused .custom-control-input:valid~.custom-control-label:before{background-color:#fbb4af}.has-danger .is-focused .custom-control-input.is-valid~.valid-feedback,.has-danger .is-focused .custom-control-input.is-valid~.valid-tooltip,.was-validated .has-danger .is-focused .custom-control-input:valid~.valid-feedback,.was-validated .has-danger .is-focused .custom-control-input:valid~.valid-tooltip{display:block}.has-danger .is-focused .custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .has-danger .is-focused .custom-control-input:valid:checked~.custom-control-label:before{background-color:#f77066}.has-danger .is-focused .custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .has-danger .is-focused .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem rgba(244,67,54,.25)}.has-danger .is-focused .custom-file-input.is-valid~.custom-file-label,.was-validated .has-danger .is-focused .custom-file-input:valid~.custom-file-label{border-color:#f44336}.has-danger .is-focused .custom-file-input.is-valid~.custom-file-label:before,.was-validated .has-danger .is-focused .custom-file-input:valid~.custom-file-label:before{border-color:inherit}.has-danger .is-focused .custom-file-input.is-valid~.valid-feedback,.has-danger .is-focused .custom-file-input.is-valid~.valid-tooltip,.was-validated .has-danger .is-focused .custom-file-input:valid~.valid-feedback,.was-validated .has-danger .is-focused .custom-file-input:valid~.valid-tooltip{display:block}.has-danger .is-focused .custom-file-input.is-valid:focus~.custom-file-label,.was-validated .has-danger .is-focused .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(244,67,54,.25)}.has-danger .is-focused .bmd-label-placeholder,.has-danger .is-focused [class*=" bmd-label"],.has-danger .is-focused [class^=bmd-label]{color:#f44336}.has-danger .is-focused .form-control{border-color:#f44336}.has-danger .is-focused .bmd-help{color:#555}.has-rose [class*=" bmd-label"],.has-rose [class^=bmd-label]{color:#e91e63}.has-rose .form-control,.is-focused .has-rose .form-control{background-image:linear-gradient(0deg,#e91e63 2px,rgba(233,30,99,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-rose .form-control:invalid{background-image:linear-gradient(0deg,#f44336 2px,rgba(244,67,54,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-rose .form-control:read-only{background-image:linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0)}.has-rose .form-control.disabled,.has-rose .form-control:disabled,.has-rose .form-control[disabled],fieldset[disabled][disabled] .has-rose .form-control{background-image:linear-gradient(90deg,#d2d2d2 0,#d2d2d2 30%,transparent 0,transparent);background-repeat:repeat-x;background-size:3px 1px}.has-rose .form-control.form-control-success,.is-focused .has-rose .form-control.form-control-success{background-image:linear-gradient(0deg,#e91e63 2px,rgba(233,30,99,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjNWNiODVjIiBkPSJNMjMzLjggNjEwYy0xMy4zIDAtMjYtNi0zNC0xNi44TDkwLjUgNDQ4LjhDNzYuMyA0MzAgODAgNDAzLjMgOTguOCAzODljMTguOC0xNC4yIDQ1LjUtMTAuNCA1OS44IDguNGw3MiA5NUw0NTEuMyAyNDJjMTIuNS0yMCAzOC44LTI2LjIgNTguOC0xMy43IDIwIDEyLjQgMjYgMzguNyAxMy43IDU4LjhMMjcwIDU5MGMtNy40IDEyLTIwLjIgMTkuNC0zNC4zIDIwaC0yeiIvPjwvc3ZnPg=="}.has-rose .form-control.form-control-warning,.is-focused .has-rose .form-control.form-control-warning{background-image:linear-gradient(0deg,#e91e63 2px,rgba(233,30,99,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZjBhZDRlIiBkPSJNNjAzIDY0MC4ybC0yNzguNS01MDljLTMuOC02LjYtMTAuOC0xMC42LTE4LjUtMTAuNnMtMTQuNyA0LTE4LjUgMTAuNkw5IDY0MC4yYy0zLjcgNi41LTMuNiAxNC40LjIgMjAuOCAzLjggNi41IDEwLjggMTAuNCAxOC4zIDEwLjRoNTU3YzcuNiAwIDE0LjYtNCAxOC40LTEwLjQgMy41LTYuNCAzLjYtMTQuNCAwLTIwLjh6bS0yNjYuNC0zMGgtNjEuMlY1NDloNjEuMnY2MS4yem0wLTEwN2gtNjEuMlYzMDRoNjEuMnYxOTl6Ii8+PC9zdmc+"}.has-rose .form-control.form-control-danger,.is-focused .has-rose .form-control.form-control-danger{background-image:linear-gradient(0deg,#e91e63 2px,rgba(233,30,99,0) 0),linear-gradient(0deg,#d2d2d2 1px,hsla(0,0%,82%,0) 0),"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MTIgNzkyIj48cGF0aCBmaWxsPSIjZDk1MzRmIiBkPSJNNDQ3IDU0NC40Yy0xNC40IDE0LjQtMzcuNiAxNC40LTUyIDBsLTg5LTkyLjctODkgOTIuN2MtMTQuNSAxNC40LTM3LjcgMTQuNC01MiAwLTE0LjQtMTQuNC0xNC40LTM3LjYgMC01Mmw5Mi40LTk2LjMtOTIuNC05Ni4zYy0xNC40LTE0LjQtMTQuNC0zNy42IDAtNTJzMzcuNi0xNC4zIDUyIDBsODkgOTIuOCA4OS4yLTkyLjdjMTQuNC0xNC40IDM3LjYtMTQuNCA1MiAwIDE0LjMgMTQuNCAxNC4zIDM3LjYgMCA1MkwzNTQuNiAzOTZsOTIuNCA5Ni40YzE0LjQgMTQuNCAxNC40IDM3LjYgMCA1MnoiLz48L3N2Zz4="}.has-rose .is-focused .valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#e91e63}.has-rose .is-focused .valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(233,30,99,.8);border-radius:.2rem}.has-rose .is-focused .custom-select.is-valid,.has-rose .is-focused .form-control.is-valid,.was-validated .has-rose .is-focused .custom-select:valid,.was-validated .has-rose .is-focused .form-control:valid{border-color:#e91e63}.has-rose .is-focused .custom-select.is-valid:focus,.has-rose .is-focused .form-control.is-valid:focus,.was-validated .has-rose .is-focused .custom-select:valid:focus,.was-validated .has-rose .is-focused .form-control:valid:focus{border-color:#e91e63;box-shadow:0 0 0 .2rem rgba(233,30,99,.25)}.has-rose .is-focused .custom-select.is-valid~.valid-feedback,.has-rose .is-focused .custom-select.is-valid~.valid-tooltip,.has-rose .is-focused .form-control.is-valid~.valid-feedback,.has-rose .is-focused .form-control.is-valid~.valid-tooltip,.was-validated .has-rose .is-focused .custom-select:valid~.valid-feedback,.was-validated .has-rose .is-focused .custom-select:valid~.valid-tooltip,.was-validated .has-rose .is-focused .form-control:valid~.valid-feedback,.was-validated .has-rose .is-focused .form-control:valid~.valid-tooltip{display:block}.has-rose .is-focused .form-check-input.is-valid~.form-check-label,.was-validated .has-rose .is-focused .form-check-input:valid~.form-check-label{color:#e91e63}.has-rose .is-focused .form-check-input.is-valid~.valid-feedback,.has-rose .is-focused .form-check-input.is-valid~.valid-tooltip,.was-validated .has-rose .is-focused .form-check-input:valid~.valid-feedback,.was-validated .has-rose .is-focused .form-check-input:valid~.valid-tooltip{display:block}.has-rose .is-focused .custom-control-input.is-valid~.custom-control-label,.was-validated .has-rose .is-focused .custom-control-input:valid~.custom-control-label{color:#e91e63}.has-rose .is-focused .custom-control-input.is-valid~.custom-control-label:before,.was-validated .has-rose .is-focused .custom-control-input:valid~.custom-control-label:before{background-color:#f492b4}.has-rose .is-focused .custom-control-input.is-valid~.valid-feedback,.has-rose .is-focused .custom-control-input.is-valid~.valid-tooltip,.was-validated .has-rose .is-focused .custom-control-input:valid~.valid-feedback,.was-validated .has-rose .is-focused .custom-control-input:valid~.valid-tooltip{display:block}.has-rose .is-focused .custom-control-input.is-valid:checked~.custom-control-label:before,.was-validated .has-rose .is-focused .custom-control-input:valid:checked~.custom-control-label:before{background-color:#ee4c83}.has-rose .is-focused .custom-control-input.is-valid:focus~.custom-control-label:before,.was-validated .has-rose .is-focused .custom-control-input:valid:focus~.custom-control-label:before{box-shadow:0 0 0 1px #fafafa,0 0 0 .2rem rgba(233,30,99,.25)}.has-rose .is-focused .custom-file-input.is-valid~.custom-file-label,.was-validated .has-rose .is-focused .custom-file-input:valid~.custom-file-label{border-color:#e91e63}.has-rose .is-focused .custom-file-input.is-valid~.custom-file-label:before,.was-validated .has-rose .is-focused .custom-file-input:valid~.custom-file-label:before{border-color:inherit}.has-rose .is-focused .custom-file-input.is-valid~.valid-feedback,.has-rose .is-focused .custom-file-input.is-valid~.valid-tooltip,.was-validated .has-rose .is-focused .custom-file-input:valid~.valid-feedback,.was-validated .has-rose .is-focused .custom-file-input:valid~.valid-tooltip{display:block}.has-rose .is-focused .custom-file-input.is-valid:focus~.custom-file-label,.was-validated .has-rose .is-focused .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(233,30,99,.25)}.has-rose .is-focused .bmd-label-placeholder,.has-rose .is-focused [class*=" bmd-label"],.has-rose .is-focused [class^=bmd-label]{color:#e91e63}.has-rose .is-focused .form-control{border-color:#e91e63}.has-rose .is-focused .bmd-help{color:#555}.bmd-form-group{position:relative}.bmd-form-group:not(.has-success):not(.has-danger) [class*=" bmd-label"].bmd-label-floating,.bmd-form-group:not(.has-success):not(.has-danger) [class^=bmd-label].bmd-label-floating{color:#aaa}.bmd-form-group [class*=" bmd-label"],.bmd-form-group [class^=bmd-label]{position:absolute;pointer-events:none;transition:all .3s ease}.bmd-form-group [class*=" bmd-label"].bmd-label-floating,.bmd-form-group [class^=bmd-label].bmd-label-floating{will-change:left,top,contents;margin:0;line-height:1.4;font-weight:400}.bmd-form-group.is-filled .bmd-label-placeholder{display:none}.bmd-form-group.bmd-collapse-inline{display:flex;align-items:center;padding:0;min-height:2.1em}.bmd-form-group.bmd-collapse-inline .collapse{flex:1;display:none}.bmd-form-group.bmd-collapse-inline .collapse.show{max-width:1200px}.bmd-form-group.bmd-collapse-inline .collapse.show,.bmd-form-group.bmd-collapse-inline .collapsing,.bmd-form-group.bmd-collapse-inline .width:not(.collapse){display:block}.bmd-form-group.bmd-collapse-inline .collapsing{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.bmd-form-group .form-control,.bmd-form-group input::placeholder,.bmd-form-group label{line-height:1.1}.bmd-form-group label{color:#aaa}.bmd-form-group .checkbox label,.bmd-form-group .radio label,.bmd-form-group .switch label,.bmd-form-group label.checkbox-inline,.bmd-form-group label.radio-inline{line-height:1.5}.bmd-form-group .checkbox label,.bmd-form-group .radio label,.bmd-form-group label{font-size:.875rem}.bmd-form-group .bmd-label-floating,.bmd-form-group .bmd-label-placeholder{top:.6125rem}.bmd-form-group .is-filled .bmd-label-floating,.bmd-form-group .is-focused .bmd-label-floating{top:-1rem;left:0;font-size:.6875rem}.bmd-form-group .bmd-label-static{top:.35rem;left:0;font-size:.875rem}.bmd-form-group .bmd-help{margin-top:0;font-size:.75rem}.bmd-form-group .form-control.form-control-danger,.bmd-form-group .form-control.form-control-success,.bmd-form-group .form-control.form-control-warning{background-size:0 100%,100% 100%,.9375rem .9375rem}.bmd-form-group .form-control.form-control-danger,.bmd-form-group .form-control.form-control-danger:focus,.bmd-form-group .form-control.form-control-success,.bmd-form-group .form-control.form-control-success:focus,.bmd-form-group .form-control.form-control-warning,.bmd-form-group .form-control.form-control-warning:focus,.bmd-form-group.is-focused .bmd-form-group .form-control.form-control-danger,.bmd-form-group.is-focused .bmd-form-group .form-control.form-control-success,.bmd-form-group.is-focused .bmd-form-group .form-control.form-control-warning{padding-right:0;background-repeat:no-repeat,no-repeat;background-position:bottom,50% calc(100% - 1px),center right .46875rem}.bmd-form-group .form-control.form-control-danger:focus,.bmd-form-group .form-control.form-control-success:focus,.bmd-form-group .form-control.form-control-warning:focus,.bmd-form-group.is-focused .bmd-form-group .form-control.form-control-danger,.bmd-form-group.is-focused .bmd-form-group .form-control.form-control-success,.bmd-form-group.is-focused .bmd-form-group .form-control.form-control-warning{background-size:100% 100%,100% 100%,.9375rem .9375rem}.bmd-form-group.bmd-form-group-sm .form-control,.bmd-form-group.bmd-form-group-sm input::placeholder,.bmd-form-group.bmd-form-group-sm label{line-height:1.1}.bmd-form-group.bmd-form-group-sm label{color:#aaa}.bmd-form-group.bmd-form-group-sm .checkbox label,.bmd-form-group.bmd-form-group-sm .radio label,.bmd-form-group.bmd-form-group-sm .switch label,.bmd-form-group.bmd-form-group-sm label.checkbox-inline,.bmd-form-group.bmd-form-group-sm label.radio-inline{line-height:1.5}.bmd-form-group.bmd-form-group-sm .checkbox label,.bmd-form-group.bmd-form-group-sm .radio label,.bmd-form-group.bmd-form-group-sm label{font-size:.875rem}.bmd-form-group.bmd-form-group-sm .bmd-label-floating,.bmd-form-group.bmd-form-group-sm .bmd-label-placeholder{top:.175rem}.bmd-form-group.bmd-form-group-sm .is-filled .bmd-label-floating,.bmd-form-group.bmd-form-group-sm .is-focused .bmd-label-floating{top:-1.25rem;left:0;font-size:.6875rem}.bmd-form-group.bmd-form-group-sm .bmd-label-static{top:.1rem;left:0;font-size:.875rem}.bmd-form-group.bmd-form-group-sm .bmd-help{margin-top:0;font-size:.65625rem}.bmd-form-group.bmd-form-group-sm .form-control.form-control-danger,.bmd-form-group.bmd-form-group-sm .form-control.form-control-success,.bmd-form-group.bmd-form-group-sm .form-control.form-control-warning{background-size:0 100%,100% 100%,.6875rem .6875rem}.bmd-form-group.bmd-form-group-sm .form-control.form-control-danger,.bmd-form-group.bmd-form-group-sm .form-control.form-control-danger:focus,.bmd-form-group.bmd-form-group-sm .form-control.form-control-success,.bmd-form-group.bmd-form-group-sm .form-control.form-control-success:focus,.bmd-form-group.bmd-form-group-sm .form-control.form-control-warning,.bmd-form-group.bmd-form-group-sm .form-control.form-control-warning:focus,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-sm .form-control.form-control-danger,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-sm .form-control.form-control-success,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-sm .form-control.form-control-warning{padding-right:0;background-repeat:no-repeat,no-repeat;background-position:bottom,50% calc(100% - 1px),center right .34375rem}.bmd-form-group.bmd-form-group-sm .form-control.form-control-danger:focus,.bmd-form-group.bmd-form-group-sm .form-control.form-control-success:focus,.bmd-form-group.bmd-form-group-sm .form-control.form-control-warning:focus,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-sm .form-control.form-control-danger,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-sm .form-control.form-control-success,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-sm .form-control.form-control-warning{background-size:100% 100%,100% 100%,.6875rem .6875rem}.bmd-form-group.bmd-form-group-lg .form-control,.bmd-form-group.bmd-form-group-lg input::placeholder,.bmd-form-group.bmd-form-group-lg label{line-height:1.1}.bmd-form-group.bmd-form-group-lg label{color:#aaa}.bmd-form-group.bmd-form-group-lg .checkbox label,.bmd-form-group.bmd-form-group-lg .radio label,.bmd-form-group.bmd-form-group-lg .switch label,.bmd-form-group.bmd-form-group-lg label.checkbox-inline,.bmd-form-group.bmd-form-group-lg label.radio-inline{line-height:1.5}.bmd-form-group.bmd-form-group-lg .checkbox label,.bmd-form-group.bmd-form-group-lg .radio label,.bmd-form-group.bmd-form-group-lg label{font-size:.875rem}.bmd-form-group.bmd-form-group-lg .bmd-label-floating,.bmd-form-group.bmd-form-group-lg .bmd-label-placeholder{top:.7375rem}.bmd-form-group.bmd-form-group-lg .is-filled .bmd-label-floating,.bmd-form-group.bmd-form-group-lg .is-focused .bmd-label-floating{top:-1rem;left:0;font-size:.6875rem}.bmd-form-group.bmd-form-group-lg .bmd-label-static{top:.35rem;left:0;font-size:.875rem}.bmd-form-group.bmd-form-group-lg .bmd-help{margin-top:0;font-size:.9375rem}.bmd-form-group.bmd-form-group-lg .form-control.form-control-danger,.bmd-form-group.bmd-form-group-lg .form-control.form-control-success,.bmd-form-group.bmd-form-group-lg .form-control.form-control-warning{background-size:0 100%,100% 100%,1.1875rem 1.1875rem}.bmd-form-group.bmd-form-group-lg .form-control.form-control-danger,.bmd-form-group.bmd-form-group-lg .form-control.form-control-danger:focus,.bmd-form-group.bmd-form-group-lg .form-control.form-control-success,.bmd-form-group.bmd-form-group-lg .form-control.form-control-success:focus,.bmd-form-group.bmd-form-group-lg .form-control.form-control-warning,.bmd-form-group.bmd-form-group-lg .form-control.form-control-warning:focus,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-lg .form-control.form-control-danger,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-lg .form-control.form-control-success,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-lg .form-control.form-control-warning{padding-right:0;background-repeat:no-repeat,no-repeat;background-position:bottom,50% calc(100% - 1px),center right .59375rem}.bmd-form-group.bmd-form-group-lg .form-control.form-control-danger:focus,.bmd-form-group.bmd-form-group-lg .form-control.form-control-success:focus,.bmd-form-group.bmd-form-group-lg .form-control.form-control-warning:focus,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-lg .form-control.form-control-danger,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-lg .form-control.form-control-success,.bmd-form-group.is-focused .bmd-form-group.bmd-form-group-lg .form-control.form-control-warning{background-size:100% 100%,100% 100%,1.1875rem 1.1875rem}.form-control,input::placeholder,label{line-height:1.1}label{color:#aaa}.checkbox label,.radio label,.switch label,label.checkbox-inline,label.radio-inline{line-height:1.5}.checkbox label,.radio label,label{font-size:.875rem}.bmd-label-floating,.bmd-label-placeholder{top:.6125rem}.is-filled .bmd-label-floating,.is-focused .bmd-label-floating{top:-1rem;left:0;font-size:.6875rem}.bmd-label-static{top:.35rem;left:0;font-size:.875rem}.bmd-help{margin-top:0;font-size:.75rem}.form-control.form-control-danger,.form-control.form-control-success,.form-control.form-control-warning{background-size:0 100%,100% 100%,.9375rem .9375rem}.bmd-form-group.is-focused .form-control.form-control-danger,.bmd-form-group.is-focused .form-control.form-control-success,.bmd-form-group.is-focused .form-control.form-control-warning,.form-control.form-control-danger,.form-control.form-control-danger:focus,.form-control.form-control-success,.form-control.form-control-success:focus,.form-control.form-control-warning,.form-control.form-control-warning:focus{padding-right:0;background-repeat:no-repeat,no-repeat;background-position:bottom,50% calc(100% - 1px),center right .46875rem}.bmd-form-group.is-focused .form-control.form-control-danger,.bmd-form-group.is-focused .form-control.form-control-success,.bmd-form-group.is-focused .form-control.form-control-warning,.form-control.form-control-danger:focus,.form-control.form-control-success:focus,.form-control.form-control-warning:focus{background-size:100% 100%,100% 100%,.9375rem .9375rem}select,select.form-control{-moz-appearance:none;-webkit-appearance:none}@media (min-width:576px){.form-inline .input-group{display:inline-flex;align-items:center}}.form-control-feedback{position:absolute;top:4px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none;opacity:0}.has-success .form-control-feedback{color:#4caf50;opacity:1}.has-danger .form-control-feedback{color:#f44336;opacity:1}.form-group{padding-bottom:10px;position:relative;margin:8px 0 0}.form-group .bmd-label-static{top:-10px}textarea{height:auto!important;resize:none;line-height:1.428571!important}.form-group input[type=file]{opacity:0;position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:-1}.form-newsletter .form-group,.form-newsletter .input-group{float:left;width:78%;margin-right:2%;margin-top:9px;padding-top:5px}.form-newsletter .btn{float:left;width:20%;margin:9px 0 0}.form-file-upload .input-group-btn:last-child>.btn-round{border-radius:30px}.form-file-upload .input-group-btn .btn{margin:0}.form-file-upload .input-group{width:100%}.input-group .input-group-btn{padding:0 12px}.form-control[disabled],.form-group .form-control[disabled],fieldset[disabled] .form-control,fieldset[disabled] .form-group .form-control{background-color:transparent;cursor:not-allowed;border-bottom:1px dotted #d2d2d2;background-repeat:no-repeat}.input-group .input-group-text{display:flex;justify-content:center;align-items:center;padding:0 15px;background-color:transparent;border-color:transparent}.img-thumbnail{border-radius:16px}.img-raised{box-shadow:0 5px 15px -8px rgba(0,0,0,.24),0 8px 10px -5px rgba(0,0,0,.2)}.rounded{border-radius:6px!important}.navbar{border:0;border-radius:3px;padding:.625rem 0;margin-bottom:20px;height:auto!important;color:#555;background-color:#fff!important;box-shadow:0 4px 18px 0 rgba(0,0,0,.12),0 7px 10px -5px rgba(0,0,0,.15)}.navbar .dropdown-item:focus,.navbar .dropdown-item:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px hsla(0,0%,100%,.4);background-color:#fff;color:#555}.navbar .navbar-toggler .navbar-toggler-icon{background-color:#555}.navbar.fixed-top{border-radius:0}.navbar .navbar-nav .nav-item .nav-link{position:relative;color:inherit;padding:.9375rem;font-weight:400;font-size:12px;text-transform:uppercase;border-radius:3px;line-height:20px}.navbar .navbar-nav .nav-item .nav-link:not(.btn-just-icon) .fa{position:relative;top:2px;margin-top:-4px;margin-right:4px}.navbar .navbar-nav .nav-item .nav-link .fa,.navbar .navbar-nav .nav-item .nav-link .material-icons{font-size:1.25rem;max-width:24px;margin-top:-1.1em}.navbar .navbar-nav .nav-item .nav-link:not(.btn) .material-icons{margin-top:-7px;top:3px;position:relative;margin-right:3px}.navbar .navbar-nav .nav-item .nav-link.profile-photo{padding:0;margin:0 3px}.navbar .navbar-nav .nav-item .nav-link.profile-photo:after{display:none}.navbar .navbar-nav .nav-item .nav-link.profile-photo .profile-photo-small{height:40px;width:40px}.navbar .navbar-nav .nav-item .nav-link.profile-photo .ripple-container{border-radius:50%}.navbar .navbar-nav .dropdown-menu-right{transform-origin:100% 0}.navbar .navbar-nav .nav-item.active .nav-link,.navbar .navbar-nav .nav-item.active .nav-link:focus,.navbar .navbar-nav .nav-item.active .nav-link:hover{color:inherit;background-color:hsla(0,0%,100%,.1)}.navbar .btn,.navbar .navbar-nav .nav-item .btn{margin-top:0;margin-bottom:0}.navbar .navbar-toggler{cursor:pointer;outline:0}.navbar .navbar-toggler .navbar-toggler-icon{width:22px;height:2px;vertical-align:middle;outline:0;display:block;border-radius:1px}.navbar .navbar-toggler .navbar-toggler-icon+.navbar-toggler-icon{margin-top:4px}.navbar.navbar-absolute{position:absolute;width:100%;padding-top:10px;z-index:1029}.navbar .navbar-wrapper{display:inline-flex;align-items:center}.navbar .navbar-brand{position:relative;color:inherit;height:50px;font-size:1.125rem;line-height:30px;padding:.625rem 0;font-weight:300;margin-left:1rem}.navbar>.container{flex:1}.navbar.bg-primary{color:#fff;background-color:#9c27b0!important;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 12px -5px rgba(156,39,176,.46)}.navbar.bg-primary .dropdown-item:focus,.navbar.bg-primary .dropdown-item:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(156,39,176,.4);background-color:#9c27b0;color:#fff}.navbar.bg-primary .navbar-toggler .navbar-toggler-icon{background-color:#fff}.navbar.bg-info{color:#fff;background-color:#00bcd4!important;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 12px -5px rgba(0,188,212,.46)}.navbar.bg-info .dropdown-item:focus,.navbar.bg-info .dropdown-item:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(0,188,212,.4);background-color:#00bcd4;color:#fff}.navbar.bg-info .navbar-toggler .navbar-toggler-icon{background-color:#fff}.navbar.bg-warning{color:#fff;background-color:#ff9800!important;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 12px -5px rgba(255,152,0,.46)}.navbar.bg-warning .dropdown-item:focus,.navbar.bg-warning .dropdown-item:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(255,152,0,.4);background-color:#ff9800;color:#fff}.navbar.bg-warning .navbar-toggler .navbar-toggler-icon{background-color:#fff}.navbar.bg-rose{color:#fff;background-color:#e91e63!important;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 12px -5px rgba(233,30,99,.46)}.navbar.bg-rose .dropdown-item:focus,.navbar.bg-rose .dropdown-item:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(233,30,99,.4);background-color:#e91e63;color:#fff}.navbar.bg-rose .navbar-toggler .navbar-toggler-icon{background-color:#fff}.navbar.bg-danger{color:#fff;background-color:#f44336!important;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 12px -5px rgba(244,67,54,.46)}.navbar.bg-danger .dropdown-item:focus,.navbar.bg-danger .dropdown-item:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(244,67,54,.4);background-color:#f44336;color:#fff}.navbar.bg-danger .navbar-toggler .navbar-toggler-icon{background-color:#fff}.navbar.bg-success{color:#fff;background-color:#4caf50!important;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 12px -5px rgba(76,175,80,.46)}.navbar.bg-success .dropdown-item:focus,.navbar.bg-success .dropdown-item:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(76,175,80,.4);background-color:#4caf50;color:#fff}.navbar.bg-success .navbar-toggler .navbar-toggler-icon{background-color:#fff}.navbar.bg-dark{color:#fff;background-color:#212121!important;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 12px -5px rgba(33,33,33,.46)}.navbar.bg-dark .dropdown-item:focus,.navbar.bg-dark .dropdown-item:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(33,33,33,.4);background-color:#212121;color:#fff}.navbar.bg-dark .navbar-toggler .navbar-toggler-icon{background-color:#fff}.navbar.navbar-transparent{background-color:transparent!important;box-shadow:none}.navbar .notification{position:absolute;top:5px;border:1px solid #fff;right:10px;font-size:9px;background:#f44336;color:#fff;min-width:20px;padding:0 5px;height:20px;border-radius:10px;text-align:center;line-height:19px;vertical-align:middle;display:block}.navbar .navbar-minimize{padding:3px 0 0 15px}.navbar .collapse .navbar-nav .nav-item .nav-link{position:relative;padding:10px 15px;font-weight:400;font-size:12px;text-transform:uppercase;border-radius:3px;line-height:20px;margin-left:5px;color:inherit}.navbar .collapse .navbar-nav .nav-item .nav-link:not(.btn-just-icon) .fa{position:relative;top:2px;margin-top:-4px;margin-right:4px}.navbar .collapse .navbar-nav .nav-item .nav-link .fa,.navbar .collapse .navbar-nav .nav-item .nav-link .material-icons{font-size:1.25rem;max-width:24px;margin-top:-1.1em}.navbar .collapse .navbar-nav .nav-item .nav-link:not(.btn) .material-icons{margin-top:-3px;top:0;position:relative;margin-right:3px}.navbar .collapse .navbar-nav .nav-item .nav-link .notification{top:0}.off-canvas-sidebar .navbar .navbar-collapse .navbar-nav .nav-item .nav-link{padding-top:15px;padding-bottom:15px;font-weight:500;font-size:12px;text-transform:uppercase;border-radius:3px;color:#fff;margin:0 15px}.off-canvas-sidebar .navbar .navbar-collapse .navbar-nav .nav-item .nav-link:hover{background:hsla(0,0%,78%,.2)}.off-canvas-sidebar .navbar.navbar-transparent{padding-top:25px!important}.alert{border:0;border-radius:3px;position:relative;padding:20px 15px;line-height:20px}.alert b{font-weight:500;text-transform:uppercase;font-size:12px}.alert,.alert.alert-default{background-color:#fff;color:#555}.alert.alert-default .alert-link,.alert.alert-default a,.alert .alert-link,.alert a{color:#555}.alert.alert-inverse{background-color:#292929;color:#fff}.alert.alert-inverse .alert-link,.alert.alert-inverse a{color:#fff}.alert.alert-primary{background-color:#a72abd;color:#fff}.alert.alert-primary .alert-link,.alert.alert-primary a{color:#fff}.alert.alert-success{background-color:#55b559;color:#fff}.alert.alert-success .alert-link,.alert.alert-success a{color:#fff}.alert.alert-info{background-color:#00cae3;color:#fff}.alert.alert-info .alert-link,.alert.alert-info a{color:#fff}.alert.alert-warning{background-color:#ff9e0f;color:#fff}.alert.alert-warning .alert-link,.alert.alert-warning a{color:#fff}.alert.alert-danger{background-color:#f55145;color:#fff}.alert.alert-danger .alert-link,.alert.alert-danger a{color:#fff}.alert.alert-rose{background-color:#ea2c6d;color:#fff}.alert-danger,.alert-info,.alert-rose,.alert-success,.alert-warning,.alert.alert-rose .alert-link,.alert.alert-rose a{color:#fff}.alert-default .alert-link,.alert-default a{color:rgba(0,0,0,.87)}.alert span{display:block;max-width:89%}.alert.alert-danger{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(244,67,54,.4)}.alert.alert-danger i{color:#f44336}.alert.alert-warning{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(255,152,0,.4)}.alert.alert-warning i{color:#ff9800}.alert.alert-success{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(76,175,80,.4)}.alert.alert-success i{color:#4caf50}.alert.alert-info{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(0,188,212,.4)}.alert.alert-info i{color:#00bcd4}.alert.alert-primary{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(156,39,176,.4)}.alert.alert-primary i{color:#9c27b0}.alert.alert-rose{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(233,30,99,.4)}.alert.alert-rose i{color:#e91e63}.alert.alert-with-icon{padding-left:66px}.alert.alert-with-icon i[data-notify=icon]{font-size:30px;display:block;left:15px;position:absolute;top:50%;margin-top:-15px;color:#fff}.alert .close{line-height:.5}.alert .close i{color:#fff;font-size:11px}.alert i[data-notify=icon]{display:none}.alert .alert-icon{display:block;float:left;margin-right:1.071rem}.alert .alert-icon i{margin-top:-7px;top:5px;position:relative}.alert [data-notify=dismiss]{margin-right:5px}.places-buttons .btn{margin-bottom:30px}.page-header{min-height:100vh;max-height:1000px;display:flex!important;height:100%;padding:0;color:#fff;position:relative}.page-header .page-header-image{position:absolute;background-size:cover;background-position:50%;width:100%;height:100%;z-index:-1}.page-header .content-center{position:absolute;top:50%;left:50%;z-index:2;transform:translate(-50%,-50%);text-align:center;color:#fff;padding:0 15px;width:100%;max-width:880px}.page-header footer{position:absolute;bottom:0;width:100%}.page-header .container{height:100%;z-index:1}.page-header .category,.page-header .description{color:hsla(0,0%,100%,.8)}.page-header.page-header-small{min-height:60vh;max-height:440px}.page-header.page-header-mini{min-height:40vh;max-height:340px}.page-header .title{margin-bottom:15px}.page-header .title+h4{margin-top:10px}.page-header:after,.page-header:before{position:absolute;z-index:0;width:100%;height:100%;display:block;left:0;top:0;content:""}.page-header:before{background-color:rgba(0,0,0,.3)}html *{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.h1,.h2,.h3,.h4,body,h1,h2,h3,h4,h5,h6{font-family:Roboto,Helvetica,Arial,sans-serif;font-weight:300;line-height:1.5em}.h1,h1{font-size:3.3125rem;line-height:1.15em}.h2,h2{font-size:2.25rem}.h3,h3{font-size:1.5625rem;margin:20px 0 10px}.h3,.h4,h3,h4{line-height:1.4em}.h4,h4{font-size:1.125rem;font-weight:300}.h5,h5{font-size:1.0625rem;line-height:1.4em;margin-bottom:15px}.h6,h6{font-size:.75rem;text-transform:uppercase;font-weight:500}.card-title,.card-title a,.footer-big h4,.footer-big h4 a,.footer-big h5,.footer-big h5 a,.footer-brand,.footer-brand a,.info-title,.info-title a,.media .media-heading,.media .media-heading a,.title,.title a{color:#3c4858;text-decoration:none}.card-blog .card-title{font-weight:700}h2.title{margin-bottom:2.142rem}.card-description,.description,.footer-big p{color:#999}.text-warning{color:#ff9800!important}.text-primary{color:#9c27b0!important}.text-danger{color:#f44336!important}.text-success{color:#4caf50!important}.text-info{color:#00bcd4!important}.text-rose{color:#e91e63!important}.text-gray{color:#999!important}.nav-tabs{border:0;border-radius:3px;padding:0 15px}.nav-tabs .nav-item .nav-link{position:relative;color:#fff;border:0;margin:0;border-radius:3px;line-height:24px;text-transform:uppercase;font-size:12px;padding:10px 15px;background-color:transparent;transition:background-color .3s 0s}.nav-tabs .nav-item .nav-link:hover{border:0}.nav-tabs .nav-item .nav-link,.nav-tabs .nav-item .nav-link:focus,.nav-tabs .nav-item .nav-link:hover{border:0!important;color:#fff!important;font-weight:500}.nav-tabs .nav-item.disabled .nav-link,.nav-tabs .nav-item.disabled .nav-link:hover{color:hsla(0,0%,100%,.5)}.nav-tabs .nav-item .material-icons{margin:-1px 5px 0 0}.nav-tabs .nav-item .nav-link.active{background-color:hsla(0,0%,100%,.2);transition:background-color .3s .2s}.nav-tabs .nav-link{border-bottom:.214rem solid transparent;color:#555}.nav-tabs .nav-link.active{color:#333;border-color:#9c27b0}.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover{border-color:#9c27b0}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link.disabled:focus,.nav-tabs .nav-link.disabled:hover{color:#999}.nav-tabs.header-primary .nav-link{color:#fff}.nav-tabs.header-primary .nav-link.active{color:#fff;border-color:#fff}.nav-tabs.header-primary .nav-link.active:focus,.nav-tabs.header-primary .nav-link.active:hover{border-color:#fff}.nav-tabs.header-primary .nav-link.disabled,.nav-tabs.header-primary .nav-link.disabled:focus,.nav-tabs.header-primary .nav-link.disabled:hover{color:hsla(0,0%,100%,.84)}.nav-tabs.bg-inverse .nav-link{color:#fff}.nav-tabs.bg-inverse .nav-link.active{color:#fff;border-color:#fff}.nav-tabs.bg-inverse .nav-link.active:focus,.nav-tabs.bg-inverse .nav-link.active:hover{border-color:#fff}.nav-tabs.bg-inverse .nav-link.disabled,.nav-tabs.bg-inverse .nav-link.disabled:focus,.nav-tabs.bg-inverse .nav-link.disabled:hover{color:hsla(0,0%,100%,.84)}.card-nav-tabs{margin-top:45px}.card-nav-tabs .card-header{margin-top:-30px!important}.tab-content .tab-pane .td-actions{display:-ms-flexbox;display:flex}.card .tab-content .form-check{margin-top:6px}.tooltip-arrow{display:none}.tooltip.show{opacity:1;transform:translateZ(0)}.tooltip{opacity:0;transition:opacity,transform .2s ease;transform:translate3d(0,5px,0);font-size:.875rem}.tooltip.bs-tooltip-auto[x-placement^=top] .arrow:before,.tooltip.bs-tooltip-top .arrow:before{border-top-color:#fff}.tooltip.bs-tooltip-auto[x-placement^=right] .arrow:before,.tooltip.bs-tooltip-right .arrow:before{border-right-color:#fff}.tooltip.bs-tooltip-auto[x-placement^=left] .arrow:before,.tooltip.bs-tooltip-left .arrow:before{border-left-color:#fff}.tooltip.bs-tooltip-auto[x-placement^=bottom] .arrow:before,.tooltip.bs-tooltip-bottom .arrow:before{border-bottom-color:#fff}.tooltip-inner{padding:10px 15px;min-width:130px}.popover,.tooltip-inner{line-height:1.5em;background:#fff;border:none;border-radius:3px;box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2);color:#555}.popover{padding:0;box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.popover.bottom>.arrow,.popover.left>.arrow,.popover.right>.arrow,.popover.top>.arrow{border:none}.popover.bs-popover-auto[x-placement^=bottom] .arrow:before,.popover.bs-popover-auto[x-placement^=left] .arrow:before,.popover.bs-popover-auto[x-placement^=right] .arrow:before,.popover.bs-popover-auto[x-placement^=top] .arrow:before,.popover.bs-popover-bottom .arrow:before,.popover.bs-popover-left .arrow:before,.popover.bs-popover-right .arrow:before,.popover.bs-popover-top .arrow:before{border:0}.popover-header{background-color:#fff;border:none;padding:15px 15px 5px;font-size:1.125rem;margin:0;color:#555}.popover-body{padding:10px 15px 15px;line-height:1.4;color:#555}.dropdown-menu{display:none;padding:.3125rem 0;border:0;opacity:0;transform:scale(0);transform-origin:0 0;will-change:transform,opacity;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.dropdown-menu.showing{animation-name:d;animation-duration:.3s;animation-fill-mode:forwards;animation-timing-function:cubic-bezier(.4,0,.2,1)}.dropdown-menu.show,.open>.dropdown-menu{display:block;opacity:1;transform:scale(1)}.dropdown-menu.hiding{display:block;opacity:0;transform:scale(0)}.dropdown-menu[x-placement=bottom-start]{transform-origin:0 0}.dropdown-menu[x-placement=bottom-end]{transform-origin:100% 0}.dropdown-menu[x-placement=top-start]{transform-origin:0 100%}.dropdown-menu[x-placement=top-end]{transform-origin:100% 100%}.dropdown-menu .disabled>a{color:#777}.dropdown-menu .disabled>a:focus,.dropdown-menu .disabled>a:hover{text-decoration:none;background-color:transparent;background-image:none;color:#777}.dropdown-menu.dropdown-with-icons .dropdown-item{padding:.75rem 1.25rem .75rem .75rem}.dropdown-menu.dropdown-with-icons .dropdown-item .material-icons{vertical-align:middle;font-size:24px;position:relative;margin-top:-4px;top:1px;margin-right:12px;opacity:.5}.dropdown-menu .dropdown-item,.dropdown-menu li>a{position:relative;width:auto;display:flex;flex-flow:nowrap;align-items:center;color:#333;font-weight:400;text-decoration:none;font-size:.8125rem;border-radius:.125rem;margin:0 .3125rem;transition:all .15s linear;min-width:7rem;padding:.625rem 1.25rem;overflow:hidden;line-height:1.428571;text-overflow:ellipsis;word-wrap:break-word}@media (min-width:768px){.dropdown-menu .dropdown-item,.dropdown-menu li>a{padding-right:1.5rem;padding-left:1.5rem}}.dropdown-menu .dropdown-item:focus,.dropdown-menu .dropdown-item:hover,.dropdown-menu a:active,.dropdown-menu a:focus,.dropdown-menu a:hover{box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(156,39,176,.4);background-color:#9c27b0;color:#fff}.btn-group.bootstrap-select.open .caret,.dropdown.open .caret,.dropup.open .caret,a[aria-expanded=true] .caret,a[data-toggle=collapse][aria-expanded=true] .caret{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);transform:rotate(180deg)}.dropdown-toggle.bmd-btn-fab:after,.dropdown-toggle.bmd-btn-icon:after{display:none}.dropdown-toggle.bmd-btn-fab~.dropdown-menu.dropdown-menu-top-left,.dropdown-toggle.bmd-btn-fab~.dropdown-menu.dropdown-menu-top-right,.dropdown-toggle.bmd-btn-icon~.dropdown-menu.dropdown-menu-top-left,.dropdown-toggle.bmd-btn-icon~.dropdown-menu.dropdown-menu-top-right{bottom:2rem}.dropdown-toggle:after{will-change:transform;transition:transform .15s linear}.dropdown-toggle.bmd-btn-fab-sm~.dropdown-menu.dropdown-menu-top-left,.dropdown-toggle.bmd-btn-fab-sm~.dropdown-menu.dropdown-menu-top-right{bottom:2.5rem}.dropdown-toggle.bmd-btn-icon~.dropdown-menu{margin:0}.show>.dropdown-toggle:not(.dropdown-item):after{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);transform:rotate(180deg)}.dropdown-header{font-size:.75rem;padding-top:.625rem;padding-bottom:.625rem;text-transform:none;color:#777;line-height:1.428571;font-weight:inherit}@keyframes d{0%{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}.dropdown-menu.bootstrap-datetimepicker-widget{opacity:0;transform:scale(0);transition-duration:.3s;transition-timing-function:cubic-bezier(.4,0,.2,1);transform-origin:0 0;will-change:transform,opacity;top:0}.dropdown-menu.bootstrap-datetimepicker-widget.top{transform-origin:0 100%}.dropdown-menu.bootstrap-datetimepicker-widget.open{opacity:1;transform:scale(1);top:0}.togglebutton{vertical-align:middle}.togglebutton,.togglebutton .toggle,.togglebutton input,.togglebutton label{user-select:none}.togglebutton label{cursor:pointer}.form-group.is-focused .togglebutton label,.togglebutton label{color:rgba(0,0,0,.26)}.form-group.is-focused .togglebutton label:focus,.form-group.is-focused .togglebutton label:hover{color:rgba(0,0,0,.54)}fieldset[disabled] .form-group.is-focused .togglebutton label{color:rgba(0,0,0,.26)}.togglebutton label input[type=checkbox]{opacity:0;width:0;height:0}.togglebutton label .toggle{text-align:left;margin-left:5px}.togglebutton label .toggle,.togglebutton label input[type=checkbox][disabled]+.toggle{content:"";display:inline-block;width:30px;height:15px;background-color:rgba(80,80,80,.7);border-radius:15px;margin-right:15px;transition:background .3s ease;vertical-align:middle}.togglebutton label .toggle:after{content:"";display:inline-block;width:20px;height:20px;background-color:#fff;border-radius:20px;position:relative;box-shadow:0 1px 3px 1px rgba(0,0,0,.4);left:-5px;top:-2.5px;border:1px solid rgba(0,0,0,.54);transition:left .3s ease,background .3s ease,box-shadow .1s ease}.togglebutton label input[type=checkbox][disabled]+.toggle:after,.togglebutton label input[type=checkbox][disabled]:checked+.toggle:after{background-color:#bdbdbd}.togglebutton label input[type=checkbox]+.toggle:active:after,.togglebutton label input[type=checkbox][disabled]+.toggle:active:after{box-shadow:0 1px 3px 1px rgba(0,0,0,.4),0 0 0 15px rgba(0,0,0,.1)}.togglebutton label input[type=checkbox]:checked+.toggle:after{left:15px}.togglebutton label input[type=checkbox]:checked+.toggle{background-color:rgba(156,39,176,.7)}.togglebutton label input[type=checkbox]:checked+.toggle:after{border-color:#9c27b0}.togglebutton label input[type=checkbox]:checked+.toggle:active:after{box-shadow:0 1px 3px 1px rgba(0,0,0,.4),0 0 0 15px rgba(156,39,176,.1)}.ripple{position:relative}.ripple-container{position:absolute;top:0;left:0;z-index:1;width:100%;height:100%;overflow:hidden;pointer-events:none;border-radius:inherit}.ripple-container .ripple-decorator{position:absolute;width:20px;height:20px;margin-top:-10px;margin-left:-10px;pointer-events:none;background-color:rgba(0,0,0,.05);border-radius:100%;opacity:0;transform:scale(1);transform-origin:50%}.ripple-container .ripple-decorator.ripple-on{opacity:.1;transition:opacity .15s ease-in 0s,transform .5s cubic-bezier(.4,0,.2,1) .1s}.ripple-container .ripple-decorator.ripple-out{opacity:0;transition:opacity .1s linear 0s!important}.footer{padding:.9375rem 0;text-align:center;display:flex}.footer ul{margin-bottom:0;padding:0;list-style:none}.footer ul li{display:inline-block}.footer ul li a{color:inherit;padding:.9375rem;font-weight:500;font-size:12px;text-transform:uppercase;border-radius:3px;position:relative;display:block}.footer ul li a,.footer ul li a:hover{text-decoration:none}.footer ul li .btn{margin:0}.footer ul.links-horizontal:first-child a{padding-left:0}.footer ul.links-horizontal:last-child a{padding-right:0}.footer ul.links-vertical li{display:block;margin-left:-5px;margin-right:-5px}.footer ul.links-vertical li a{padding:5px}.footer .social-buttons .btn,.footer .social-buttons a{margin-top:5px;margin-bottom:5px}.footer .footer-brand{float:left;height:50px;padding:15px;font-size:18px;line-height:20px;margin-left:-15px}.footer .footer-brand:focus,.footer .footer-brand:hover{color:#3c4858}.footer .copyright{padding:15px 0}.footer .copyright .material-icons{font-size:18px;position:relative;top:3px}.footer .pull-center{display:inline-block;float:none}.off-canvas-sidebar .footer{position:absolute;bottom:0;width:100%}@media screen and (min-width:768px){.footer .copyright{padding-right:15px}}.wrapper{position:relative;top:0;height:100vh}.sidebar{position:fixed;top:0;bottom:0;left:0;z-index:2;width:260px;background:#fff;box-shadow:0 16px 38px -12px rgba(0,0,0,.56),0 4px 25px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.sidebar .caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.sidebar[data-background-color=black]{background-color:#191919}.sidebar .sidebar-wrapper{position:relative;height:calc(100vh - 75px);overflow:auto;width:260px;z-index:4;padding-bottom:30px}.sidebar .sidebar-wrapper .dropdown .dropdown-backdrop{display:none!important}.sidebar .sidebar-wrapper .navbar-form{border:none;box-shadow:none}.sidebar .sidebar-wrapper .navbar-form .input-group{font-size:1.7em;height:36px;width:78%;padding-left:17px}.sidebar .sidebar-wrapper .user .user-info [data-toggle=collapse]~div>ul>li>a span,.sidebar .sidebar-wrapper>.nav [data-toggle=collapse]~div>ul>li>a span{display:inline-block}.sidebar .sidebar-wrapper .user .user-info [data-toggle=collapse]~div>ul>li>a .sidebar-normal,.sidebar .sidebar-wrapper>.nav [data-toggle=collapse]~div>ul>li>a .sidebar-normal{margin:0;position:relative;transform:translateX(0);opacity:1;white-space:nowrap;display:block}.sidebar .sidebar-wrapper .user .user-info [data-toggle=collapse]~div>ul>li>a .sidebar-mini,.sidebar .sidebar-wrapper>.nav [data-toggle=collapse]~div>ul>li>a .sidebar-mini{text-transform:uppercase;width:30px;margin-right:15px;text-align:center;letter-spacing:1px;position:relative;float:left;display:inherit}.sidebar .sidebar-wrapper .user .user-info [data-toggle=collapse]~div>ul>li>a i,.sidebar .sidebar-wrapper>.nav [data-toggle=collapse]~div>ul>li>a i{font-size:17px;line-height:20px;width:26px}.sidebar .nav{margin-top:20px;display:block}.sidebar .nav .caret{margin-top:13px;position:absolute;right:6px}.sidebar .nav li>a:focus,.sidebar .nav li>a:hover{background-color:transparent;outline:none}.sidebar .nav li:first-child>a{margin:0 15px}.sidebar .nav li.active>[data-toggle=collapse],.sidebar .nav li .dropdown-menu a:focus,.sidebar .nav li .dropdown-menu a:hover,.sidebar .nav li:hover>a{background-color:hsla(0,0%,78%,.2);color:#3c4858;box-shadow:none}.sidebar .nav li.active>[data-toggle=collapse] i{color:#a9afbb}.sidebar .nav li.active>a,.sidebar .nav li.active>a i{color:#fff}.sidebar .nav li.separator{margin:15px 0}.sidebar .nav li.separator:after{width:calc(100% - 30px);content:"";position:absolute;height:1px;left:15px;background-color:hsla(0,0%,71%,.3)}.sidebar .nav li.separator+li{margin-top:31px}.sidebar .nav p{margin:0;line-height:30px;font-size:14px;position:relative;display:block;height:auto;white-space:nowrap}.sidebar .nav i{font-size:24px;float:left;margin-right:15px;line-height:30px;width:30px;text-align:center;color:#a9afbb}.sidebar .nav li .dropdown-menu a,.sidebar .nav li a{margin:10px 15px 0;border-radius:3px;color:#3c4858;padding-left:10px;padding-right:10px;text-transform:capitalize;font-size:13px;padding:10px 15px}.sidebar .sidebar-background{position:absolute;z-index:1;height:100%;width:100%;display:block;top:0;left:0;background-size:cover;background-position:50%}.sidebar .sidebar-background:after{position:absolute;z-index:3;width:100%;height:100%;content:"";display:block;background:#fff;opacity:.93}.sidebar .logo{padding:15px 0;margin:0;display:block;position:relative;z-index:4}.sidebar .logo:after{content:"";position:absolute;bottom:0;right:15px;height:1px;width:calc(100% - 30px);background-color:hsla(0,0%,71%,.3)}.sidebar .logo p{float:left;font-size:20px;margin:10px;color:#fff;line-height:20px}.sidebar .logo .simple-text{text-transform:uppercase;padding:5px 0;display:inline-block;font-size:18px;color:#3c4858;white-space:nowrap;font-weight:400;line-height:30px;overflow:hidden;text-align:center;display:block}.sidebar .logo-tim{border-radius:50%;border:1px solid #333;display:block;height:61px;width:61px;float:left;overflow:hidden}.sidebar .logo-tim img{width:60px;height:60px}.sidebar[data-background-color=black] .nav .nav-item .nav-link{color:#fff}.sidebar[data-background-color=black] .nav .nav-item i{color:hsla(0,0%,100%,.8)}.sidebar[data-background-color=black] .nav .nav-item.active [data-toggle=collapse],.sidebar[data-background-color=black] .nav .nav-item:hover [data-toggle=collapse]{color:#fff}.sidebar[data-background-color=black] .nav .nav-item.active [data-toggle=collapse] i,.sidebar[data-background-color=black] .nav .nav-item:hover [data-toggle=collapse] i{color:hsla(0,0%,100%,.8)}.sidebar[data-background-color=black] .simple-text,.sidebar[data-background-color=black] .user a{color:#fff}.sidebar[data-background-color=black] .sidebar-background:after{background:#000;opacity:.8}.sidebar[data-background-color=black] .nav li .dropdown-menu .dropdown-item{color:#fff}.sidebar[data-color=purple] li.active>a{background-color:#9c27b0;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(156,39,176,.4)}.sidebar[data-color=azure] li.active>a{background-color:#00bcd4;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(0,188,212,.4)}.sidebar[data-color=green] li.active>a{background-color:#4caf50;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(76,175,80,.4)}.sidebar[data-color=orange] li.active>a{background-color:#ff9800;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(255,152,0,.4)}.sidebar[data-color=danger] li.active>a{background-color:#f44336;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(244,67,54,.4)}.sidebar[data-color=rose] li.active>a{background-color:#e91e63;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(233,30,99,.4)}.sidebar[data-color=white] li.active>a{background-color:#fff;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px hsla(0,0%,100%,.4)}.sidebar[data-color=white] .nav .nav-item.active>a:not([data-toggle=collapse]){color:#3c4858;opacity:1;box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(60,72,88,.4)}.sidebar[data-color=white] .nav .nav-item.active>a:not([data-toggle=collapse]) i{color:rgba(60,72,88,.8)}.sidebar[data-background-color=red] .nav .nav-item .nav-link{color:#fff}.sidebar[data-background-color=red] .nav .nav-item i{color:hsla(0,0%,100%,.8)}.sidebar[data-background-color=red] .nav .nav-item.active [data-toggle=collapse],.sidebar[data-background-color=red] .nav .nav-item:hover [data-toggle=collapse]{color:#fff}.sidebar[data-background-color=red] .nav .nav-item.active [data-toggle=collapse] i,.sidebar[data-background-color=red] .nav .nav-item:hover [data-toggle=collapse] i{color:hsla(0,0%,100%,.8)}.sidebar[data-background-color=red] .simple-text,.sidebar[data-background-color=red] .user a{color:#fff}.sidebar[data-background-color=red] .sidebar-background:after{background:#f44336;opacity:.8}.sidebar[data-background-color=red] .logo:after,.sidebar[data-background-color=red] .nav li.separator:after,.sidebar[data-background-color=red] .user:after{background-color:hsla(0,0%,100%,.3)}.sidebar[data-background-color=red] .nav li.active>[data-toggle=collapse],.sidebar[data-background-color=red] .nav li:hover:not(.active)>a{background-color:hsla(0,0%,100%,.1)}.sidebar.has-image:after,.sidebar[data-image]:after{opacity:.77}.off-canvas-sidebar .navbar-collapse .nav>li>a,.off-canvas-sidebar .navbar-collapse .nav>li>a:hover{color:#fff;margin:0 15px}.off-canvas-sidebar .navbar-collapse .nav>li>a:focus,.off-canvas-sidebar .navbar-collapse .nav>li>a:hover{background:hsla(0,0%,78%,.2)}.main-panel{position:relative;float:right;width:calc(100% - 260px);transition:.33s,cubic-bezier(.685,.0473,.346,1)}.main-panel>.content{margin-top:70px;padding:30px 15px;min-height:calc(100vh - 123px)}.main-panel>.footer{border-top:1px solid #e7e7e7}.main-panel>.navbar{margin-bottom:0}.main-panel .header{margin-bottom:30px}.main-panel .header .title{margin-top:10px;margin-bottom:10px}.perfect-scrollbar-on .main-panel,.perfect-scrollbar-on .sidebar{height:100%;max-height:100%}.main-panel,.sidebar,.sidebar-wrapper{transition-property:top,bottom,width;transition-duration:.2s,.2s,.35s;transition-timing-function:linear,linear,ease;-webkit-overflow-scrolling:touch}.visible-on-sidebar-regular{display:inline-block!important}.visible-on-sidebar-mini{display:none!important}@media (min-width:991px){.sidebar-mini .visible-on-sidebar-regular{display:none!important}.sidebar-mini .visible-on-sidebar-mini{display:inline-block!important}.sidebar-mini .sidebar,.sidebar-mini .sidebar .sidebar-wrapper{width:80px}.sidebar-mini .main-panel{width:calc(100% - 80px)}.sidebar-mini .sidebar{display:block;font-weight:200;z-index:9999}.sidebar-mini .sidebar .logo a.logo-normal,.sidebar-mini .sidebar .sidebar-wrapper .user .user-info>a>span,.sidebar-mini .sidebar .sidebar-wrapper .user .user-info [data-toggle=collapse]~div>ul>li>a .sidebar-normal,.sidebar-mini .sidebar .sidebar-wrapper>.nav [data-toggle=collapse]~div>ul>li>a .sidebar-normal,.sidebar-mini .sidebar .sidebar-wrapper>.nav li>a p{opacity:0;transform:translate3d(-25px,0,0)}.sidebar-mini .sidebar:hover{width:260px}.sidebar-mini .sidebar:hover .logo a.logo-normal{opacity:1;transform:translateZ(0)}.sidebar-mini .sidebar:hover .sidebar-wrapper{width:260px}.sidebar-mini .sidebar:hover .sidebar-wrapper .user .user-info>a>span,.sidebar-mini .sidebar:hover .sidebar-wrapper .user .user-info [data-toggle=collapse]~div>ul>li>a .sidebar-normal,.sidebar-mini .sidebar:hover .sidebar-wrapper>.nav [data-toggle=collapse]~div>ul>li>a .sidebar-normal,.sidebar-mini .sidebar:hover .sidebar-wrapper>.nav li>a p{transform:translateZ(0);opacity:1}.sidebar .nav .nav-item.active-pro{position:absolute;width:100%;bottom:13px;left:0}}.fixed-plugin .dropdown .dropdown-menu{border-radius:10px}.fixed-plugin .dropdown .dropdown-menu li.adjustments-line{border-bottom:1px solid #ddd}.fixed-plugin .dropdown .dropdown-menu li{padding:5px 2px!important}.fixed-plugin .dropdown .dropdown-menu .adjustments-line .bootstrap-switch{position:absolute;right:10px!important}.fixed-plugin .dropdown .dropdown-menu .adjustments-line label{margin-bottom:.1rem!important}.fixed-plugin .badge,.fixed-plugin li>a{transition:all .34s;-webkit-transition:all .34s;-moz-transition:all .34s}.fixed-plugin{position:fixed;top:115px;right:0;width:64px;background:rgba(0,0,0,.3);z-index:3;border-radius:8px 0 0 8px;text-align:center}.fixed-plugin .fa-cog{color:#fff;padding:10px;border-radius:0 0 6px 6px;width:auto}.fixed-plugin .dropdown-menu{right:80px;left:auto;width:290px;border-radius:.1875rem;padding:0 10px}.fixed-plugin .dropdown-menu:after,.fixed-plugin .dropdown-menu:before{right:10px;margin-left:auto;left:auto}.fixed-plugin .fa-circle-thin{color:#fff}.fixed-plugin .active .fa-circle-thin{color:#0bf}.fixed-plugin .dropdown-menu>.active>a,.fixed-plugin .dropdown-menu>.active>a:focus,.fixed-plugin .dropdown-menu>.active>a:hover{color:#777;text-align:center}.fixed-plugin img{border-radius:0;width:100%;height:100px;margin:0 auto}.fixed-plugin .dropdown-menu li>a:focus,.fixed-plugin .dropdown-menu li>a:hover{box-shadow:none}.fixed-plugin .badge{border:3px solid #fff;border-radius:50%;cursor:pointer;display:inline-block;height:23px;margin-right:5px;position:relative;width:23px;padding:8px}.fixed-plugin .badge.active,.fixed-plugin .badge:hover{border-color:#0bf}.fixed-plugin .badge-black{background-color:#000}.fixed-plugin .badge-azure{background-color:#2ca8ff}.fixed-plugin .badge-green{background-color:#18ce0f}.fixed-plugin .badge-orange{background-color:#f96332}.fixed-plugin .badge-yellow{background-color:#ffb236}.fixed-plugin .badge-danger{background-color:#f44336}.fixed-plugin .badge-purple{background-color:#9368e9}.fixed-plugin .badge-white{background-color:hsla(0,0%,78%,.2)}.fixed-plugin .badge-rose{background-color:#e91e63}.fixed-plugin h5{font-size:14px;margin:10px}.fixed-plugin .dropdown-menu li{display:block;padding:18px 2px;width:25%;float:left}.fixed-plugin li.adjustments-line,.fixed-plugin li.button-container,.fixed-plugin li.header-title{width:100%;height:50px;min-height:inherit}.fixed-plugin li.button-container{height:auto}.fixed-plugin li.button-container div{margin-bottom:5px}.fixed-plugin .btn{position:relative;padding:12px 30px;margin:.3125rem 1px;font-size:.75rem;border-radius:.2rem;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1);will-change:box-shadow,transform}.fixed-plugin .btn.btn-primary{color:#fff;background-color:#9c27b0;border-color:#9c27b0;box-shadow:0 2px 2px 0 rgba(156,39,176,.14),0 3px 1px -2px rgba(156,39,176,.2),0 1px 5px 0 rgba(156,39,176,.12)}.fixed-plugin .btn.btn-primary.focus,.fixed-plugin .btn.btn-primary:focus,.fixed-plugin .btn.btn-primary:hover{color:#fff;background-color:#9124a3;border-color:#701c7e}.fixed-plugin .btn.btn-primary.active,.fixed-plugin .btn.btn-primary:active,.open>.fixed-plugin .btn.btn-primary.dropdown-toggle,.show>.fixed-plugin .btn.btn-primary.dropdown-toggle{color:#fff;background-color:#9124a3;border-color:#701c7e;box-shadow:0 2px 2px 0 rgba(156,39,176,.14),0 3px 1px -2px rgba(156,39,176,.2),0 1px 5px 0 rgba(156,39,176,.12)}.fixed-plugin .btn.btn-primary.active.focus,.fixed-plugin .btn.btn-primary.active:focus,.fixed-plugin .btn.btn-primary.active:hover,.fixed-plugin .btn.btn-primary:active.focus,.fixed-plugin .btn.btn-primary:active:focus,.fixed-plugin .btn.btn-primary:active:hover,.open>.fixed-plugin .btn.btn-primary.dropdown-toggle.focus,.open>.fixed-plugin .btn.btn-primary.dropdown-toggle:focus,.open>.fixed-plugin .btn.btn-primary.dropdown-toggle:hover,.show>.fixed-plugin .btn.btn-primary.dropdown-toggle.focus,.show>.fixed-plugin .btn.btn-primary.dropdown-toggle:focus,.show>.fixed-plugin .btn.btn-primary.dropdown-toggle:hover{color:#fff;background-color:#9124a3;border-color:#3f1048}.open>.fixed-plugin .btn.btn-primary.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#9c27b0}.open>.fixed-plugin .btn.btn-primary.dropdown-toggle.bmd-btn-icon:hover{background-color:#9124a3}.fixed-plugin .btn.btn-primary.disabled.focus,.fixed-plugin .btn.btn-primary.disabled:focus,.fixed-plugin .btn.btn-primary.disabled:hover,.fixed-plugin .btn.btn-primary:disabled.focus,.fixed-plugin .btn.btn-primary:disabled:focus,.fixed-plugin .btn.btn-primary:disabled:hover{background-color:#9c27b0;border-color:#9c27b0}.fixed-plugin .btn.btn-primary:active,.fixed-plugin .btn.btn-primary:focus,.fixed-plugin .btn.btn-primary:hover{box-shadow:0 14px 26px -12px rgba(156,39,176,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(156,39,176,.2)}.fixed-plugin .btn.btn-primary.btn-link{box-shadow:none}.fixed-plugin .btn.btn-primary.btn-link,.fixed-plugin .btn.btn-primary.btn-link:active,.fixed-plugin .btn.btn-primary.btn-link:focus,.fixed-plugin .btn.btn-primary.btn-link:hover{background-color:transparent;color:#9c27b0}.fixed-plugin .btn.btn-secondary{color:#333;background-color:#fafafa;border-color:#ccc;box-shadow:0 2px 2px 0 hsla(0,0%,98%,.14),0 3px 1px -2px hsla(0,0%,98%,.2),0 1px 5px 0 hsla(0,0%,98%,.12)}.fixed-plugin .btn.btn-secondary.focus,.fixed-plugin .btn.btn-secondary:focus,.fixed-plugin .btn.btn-secondary:hover{color:#333;background-color:#f2f2f2;border-color:#adadad}.fixed-plugin .btn.btn-secondary.active,.fixed-plugin .btn.btn-secondary:active,.open>.fixed-plugin .btn.btn-secondary.dropdown-toggle,.show>.fixed-plugin .btn.btn-secondary.dropdown-toggle{color:#333;background-color:#f2f2f2;border-color:#adadad;box-shadow:0 2px 2px 0 hsla(0,0%,98%,.14),0 3px 1px -2px hsla(0,0%,98%,.2),0 1px 5px 0 hsla(0,0%,98%,.12)}.fixed-plugin .btn.btn-secondary.active.focus,.fixed-plugin .btn.btn-secondary.active:focus,.fixed-plugin .btn.btn-secondary.active:hover,.fixed-plugin .btn.btn-secondary:active.focus,.fixed-plugin .btn.btn-secondary:active:focus,.fixed-plugin .btn.btn-secondary:active:hover,.open>.fixed-plugin .btn.btn-secondary.dropdown-toggle.focus,.open>.fixed-plugin .btn.btn-secondary.dropdown-toggle:focus,.open>.fixed-plugin .btn.btn-secondary.dropdown-toggle:hover,.show>.fixed-plugin .btn.btn-secondary.dropdown-toggle.focus,.show>.fixed-plugin .btn.btn-secondary.dropdown-toggle:focus,.show>.fixed-plugin .btn.btn-secondary.dropdown-toggle:hover{color:#333;background-color:#f2f2f2;border-color:#8c8c8c}.open>.fixed-plugin .btn.btn-secondary.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#fafafa}.open>.fixed-plugin .btn.btn-secondary.dropdown-toggle.bmd-btn-icon:hover{background-color:#f2f2f2}.fixed-plugin .btn.btn-secondary.disabled.focus,.fixed-plugin .btn.btn-secondary.disabled:focus,.fixed-plugin .btn.btn-secondary.disabled:hover,.fixed-plugin .btn.btn-secondary:disabled.focus,.fixed-plugin .btn.btn-secondary:disabled:focus,.fixed-plugin .btn.btn-secondary:disabled:hover{background-color:#fafafa;border-color:#ccc}.fixed-plugin .btn.btn-secondary:active,.fixed-plugin .btn.btn-secondary:focus,.fixed-plugin .btn.btn-secondary:hover{box-shadow:0 14px 26px -12px hsla(0,0%,98%,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px hsla(0,0%,98%,.2)}.fixed-plugin .btn.btn-secondary.btn-link{box-shadow:none}.fixed-plugin .btn.btn-secondary.btn-link,.fixed-plugin .btn.btn-secondary.btn-link:active,.fixed-plugin .btn.btn-secondary.btn-link:focus,.fixed-plugin .btn.btn-secondary.btn-link:hover{background-color:transparent;color:#fafafa}.fixed-plugin .btn.btn-info{color:#fff;background-color:#00bcd4;border-color:#00bcd4;box-shadow:0 2px 2px 0 rgba(0,188,212,.14),0 3px 1px -2px rgba(0,188,212,.2),0 1px 5px 0 rgba(0,188,212,.12)}.fixed-plugin .btn.btn-info.focus,.fixed-plugin .btn.btn-info:focus,.fixed-plugin .btn.btn-info:hover{color:#fff;background-color:#00aec5;border-color:#008697}.fixed-plugin .btn.btn-info.active,.fixed-plugin .btn.btn-info:active,.open>.fixed-plugin .btn.btn-info.dropdown-toggle,.show>.fixed-plugin .btn.btn-info.dropdown-toggle{color:#fff;background-color:#00aec5;border-color:#008697;box-shadow:0 2px 2px 0 rgba(0,188,212,.14),0 3px 1px -2px rgba(0,188,212,.2),0 1px 5px 0 rgba(0,188,212,.12)}.fixed-plugin .btn.btn-info.active.focus,.fixed-plugin .btn.btn-info.active:focus,.fixed-plugin .btn.btn-info.active:hover,.fixed-plugin .btn.btn-info:active.focus,.fixed-plugin .btn.btn-info:active:focus,.fixed-plugin .btn.btn-info:active:hover,.open>.fixed-plugin .btn.btn-info.dropdown-toggle.focus,.open>.fixed-plugin .btn.btn-info.dropdown-toggle:focus,.open>.fixed-plugin .btn.btn-info.dropdown-toggle:hover,.show>.fixed-plugin .btn.btn-info.dropdown-toggle.focus,.show>.fixed-plugin .btn.btn-info.dropdown-toggle:focus,.show>.fixed-plugin .btn.btn-info.dropdown-toggle:hover{color:#fff;background-color:#00aec5;border-color:#004b55}.open>.fixed-plugin .btn.btn-info.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#00bcd4}.open>.fixed-plugin .btn.btn-info.dropdown-toggle.bmd-btn-icon:hover{background-color:#00aec5}.fixed-plugin .btn.btn-info.disabled.focus,.fixed-plugin .btn.btn-info.disabled:focus,.fixed-plugin .btn.btn-info.disabled:hover,.fixed-plugin .btn.btn-info:disabled.focus,.fixed-plugin .btn.btn-info:disabled:focus,.fixed-plugin .btn.btn-info:disabled:hover{background-color:#00bcd4;border-color:#00bcd4}.fixed-plugin .btn.btn-info:active,.fixed-plugin .btn.btn-info:focus,.fixed-plugin .btn.btn-info:hover{box-shadow:0 14px 26px -12px rgba(0,188,212,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(0,188,212,.2)}.fixed-plugin .btn.btn-info.btn-link{box-shadow:none}.fixed-plugin .btn.btn-info.btn-link,.fixed-plugin .btn.btn-info.btn-link:active,.fixed-plugin .btn.btn-info.btn-link:focus,.fixed-plugin .btn.btn-info.btn-link:hover{background-color:transparent;color:#00bcd4}.fixed-plugin .btn.btn-success{color:#fff;background-color:#4caf50;border-color:#4caf50;box-shadow:0 2px 2px 0 rgba(76,175,80,.14),0 3px 1px -2px rgba(76,175,80,.2),0 1px 5px 0 rgba(76,175,80,.12)}.fixed-plugin .btn.btn-success.focus,.fixed-plugin .btn.btn-success:focus,.fixed-plugin .btn.btn-success:hover{color:#fff;background-color:#47a44b;border-color:#39843c}.fixed-plugin .btn.btn-success.active,.fixed-plugin .btn.btn-success:active,.open>.fixed-plugin .btn.btn-success.dropdown-toggle,.show>.fixed-plugin .btn.btn-success.dropdown-toggle{color:#fff;background-color:#47a44b;border-color:#39843c;box-shadow:0 2px 2px 0 rgba(76,175,80,.14),0 3px 1px -2px rgba(76,175,80,.2),0 1px 5px 0 rgba(76,175,80,.12)}.fixed-plugin .btn.btn-success.active.focus,.fixed-plugin .btn.btn-success.active:focus,.fixed-plugin .btn.btn-success.active:hover,.fixed-plugin .btn.btn-success:active.focus,.fixed-plugin .btn.btn-success:active:focus,.fixed-plugin .btn.btn-success:active:hover,.open>.fixed-plugin .btn.btn-success.dropdown-toggle.focus,.open>.fixed-plugin .btn.btn-success.dropdown-toggle:focus,.open>.fixed-plugin .btn.btn-success.dropdown-toggle:hover,.show>.fixed-plugin .btn.btn-success.dropdown-toggle.focus,.show>.fixed-plugin .btn.btn-success.dropdown-toggle:focus,.show>.fixed-plugin .btn.btn-success.dropdown-toggle:hover{color:#fff;background-color:#47a44b;border-color:#255627}.open>.fixed-plugin .btn.btn-success.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#4caf50}.open>.fixed-plugin .btn.btn-success.dropdown-toggle.bmd-btn-icon:hover{background-color:#47a44b}.fixed-plugin .btn.btn-success.disabled.focus,.fixed-plugin .btn.btn-success.disabled:focus,.fixed-plugin .btn.btn-success.disabled:hover,.fixed-plugin .btn.btn-success:disabled.focus,.fixed-plugin .btn.btn-success:disabled:focus,.fixed-plugin .btn.btn-success:disabled:hover{background-color:#4caf50;border-color:#4caf50}.fixed-plugin .btn.btn-success:active,.fixed-plugin .btn.btn-success:focus,.fixed-plugin .btn.btn-success:hover{box-shadow:0 14px 26px -12px rgba(76,175,80,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(76,175,80,.2)}.fixed-plugin .btn.btn-success.btn-link{box-shadow:none}.fixed-plugin .btn.btn-success.btn-link,.fixed-plugin .btn.btn-success.btn-link:active,.fixed-plugin .btn.btn-success.btn-link:focus,.fixed-plugin .btn.btn-success.btn-link:hover{background-color:transparent;color:#4caf50}.fixed-plugin .btn.btn-warning{color:#fff;background-color:#ff9800;border-color:#ff9800;box-shadow:0 2px 2px 0 rgba(255,152,0,.14),0 3px 1px -2px rgba(255,152,0,.2),0 1px 5px 0 rgba(255,152,0,.12)}.fixed-plugin .btn.btn-warning.focus,.fixed-plugin .btn.btn-warning:focus,.fixed-plugin .btn.btn-warning:hover{color:#fff;background-color:#f08f00;border-color:#c27400}.fixed-plugin .btn.btn-warning.active,.fixed-plugin .btn.btn-warning:active,.open>.fixed-plugin .btn.btn-warning.dropdown-toggle,.show>.fixed-plugin .btn.btn-warning.dropdown-toggle{color:#fff;background-color:#f08f00;border-color:#c27400;box-shadow:0 2px 2px 0 rgba(255,152,0,.14),0 3px 1px -2px rgba(255,152,0,.2),0 1px 5px 0 rgba(255,152,0,.12)}.fixed-plugin .btn.btn-warning.active.focus,.fixed-plugin .btn.btn-warning.active:focus,.fixed-plugin .btn.btn-warning.active:hover,.fixed-plugin .btn.btn-warning:active.focus,.fixed-plugin .btn.btn-warning:active:focus,.fixed-plugin .btn.btn-warning:active:hover,.open>.fixed-plugin .btn.btn-warning.dropdown-toggle.focus,.open>.fixed-plugin .btn.btn-warning.dropdown-toggle:focus,.open>.fixed-plugin .btn.btn-warning.dropdown-toggle:hover,.show>.fixed-plugin .btn.btn-warning.dropdown-toggle.focus,.show>.fixed-plugin .btn.btn-warning.dropdown-toggle:focus,.show>.fixed-plugin .btn.btn-warning.dropdown-toggle:hover{color:#fff;background-color:#f08f00;border-color:#804c00}.open>.fixed-plugin .btn.btn-warning.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#ff9800}.open>.fixed-plugin .btn.btn-warning.dropdown-toggle.bmd-btn-icon:hover{background-color:#f08f00}.fixed-plugin .btn.btn-warning.disabled.focus,.fixed-plugin .btn.btn-warning.disabled:focus,.fixed-plugin .btn.btn-warning.disabled:hover,.fixed-plugin .btn.btn-warning:disabled.focus,.fixed-plugin .btn.btn-warning:disabled:focus,.fixed-plugin .btn.btn-warning:disabled:hover{background-color:#ff9800;border-color:#ff9800}.fixed-plugin .btn.btn-warning:active,.fixed-plugin .btn.btn-warning:focus,.fixed-plugin .btn.btn-warning:hover{box-shadow:0 14px 26px -12px rgba(255,152,0,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(255,152,0,.2)}.fixed-plugin .btn.btn-warning.btn-link{box-shadow:none}.fixed-plugin .btn.btn-warning.btn-link,.fixed-plugin .btn.btn-warning.btn-link:active,.fixed-plugin .btn.btn-warning.btn-link:focus,.fixed-plugin .btn.btn-warning.btn-link:hover{background-color:transparent;color:#ff9800}.fixed-plugin .btn.btn-danger{color:#fff;background-color:#f44336;border-color:#f44336;box-shadow:0 2px 2px 0 rgba(244,67,54,.14),0 3px 1px -2px rgba(244,67,54,.2),0 1px 5px 0 rgba(244,67,54,.12)}.fixed-plugin .btn.btn-danger.focus,.fixed-plugin .btn.btn-danger:focus,.fixed-plugin .btn.btn-danger:hover{color:#fff;background-color:#f33527;border-color:#e11b0c}.fixed-plugin .btn.btn-danger.active,.fixed-plugin .btn.btn-danger:active,.open>.fixed-plugin .btn.btn-danger.dropdown-toggle,.show>.fixed-plugin .btn.btn-danger.dropdown-toggle{color:#fff;background-color:#f33527;border-color:#e11b0c;box-shadow:0 2px 2px 0 rgba(244,67,54,.14),0 3px 1px -2px rgba(244,67,54,.2),0 1px 5px 0 rgba(244,67,54,.12)}.fixed-plugin .btn.btn-danger.active.focus,.fixed-plugin .btn.btn-danger.active:focus,.fixed-plugin .btn.btn-danger.active:hover,.fixed-plugin .btn.btn-danger:active.focus,.fixed-plugin .btn.btn-danger:active:focus,.fixed-plugin .btn.btn-danger:active:hover,.open>.fixed-plugin .btn.btn-danger.dropdown-toggle.focus,.open>.fixed-plugin .btn.btn-danger.dropdown-toggle:focus,.open>.fixed-plugin .btn.btn-danger.dropdown-toggle:hover,.show>.fixed-plugin .btn.btn-danger.dropdown-toggle.focus,.show>.fixed-plugin .btn.btn-danger.dropdown-toggle:focus,.show>.fixed-plugin .btn.btn-danger.dropdown-toggle:hover{color:#fff;background-color:#f33527;border-color:#a21309}.open>.fixed-plugin .btn.btn-danger.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#f44336}.open>.fixed-plugin .btn.btn-danger.dropdown-toggle.bmd-btn-icon:hover{background-color:#f33527}.fixed-plugin .btn.btn-danger.disabled.focus,.fixed-plugin .btn.btn-danger.disabled:focus,.fixed-plugin .btn.btn-danger.disabled:hover,.fixed-plugin .btn.btn-danger:disabled.focus,.fixed-plugin .btn.btn-danger:disabled:focus,.fixed-plugin .btn.btn-danger:disabled:hover{background-color:#f44336;border-color:#f44336}.fixed-plugin .btn.btn-danger:active,.fixed-plugin .btn.btn-danger:focus,.fixed-plugin .btn.btn-danger:hover{box-shadow:0 14px 26px -12px rgba(244,67,54,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(244,67,54,.2)}.fixed-plugin .btn.btn-danger.btn-link{box-shadow:none}.fixed-plugin .btn.btn-danger.btn-link,.fixed-plugin .btn.btn-danger.btn-link:active,.fixed-plugin .btn.btn-danger.btn-link:focus,.fixed-plugin .btn.btn-danger.btn-link:hover{background-color:transparent;color:#f44336}.fixed-plugin .btn.btn-rose{color:#fff;background-color:#e91e63;border-color:#e91e63;box-shadow:0 2px 2px 0 rgba(233,30,99,.14),0 3px 1px -2px rgba(233,30,99,.2),0 1px 5px 0 rgba(233,30,99,.12)}.fixed-plugin .btn.btn-rose.focus,.fixed-plugin .btn.btn-rose:focus,.fixed-plugin .btn.btn-rose:hover{color:#fff;background-color:#ea2c6d;border-color:#b8124a}.fixed-plugin .btn.btn-rose.active,.fixed-plugin .btn.btn-rose:active,.open>.fixed-plugin .btn.btn-rose.dropdown-toggle,.show>.fixed-plugin .btn.btn-rose.dropdown-toggle{color:#fff;background-color:#ea2c6d;border-color:#b8124a;box-shadow:0 2px 2px 0 rgba(233,30,99,.14),0 3px 1px -2px rgba(233,30,99,.2),0 1px 5px 0 rgba(233,30,99,.12)}.fixed-plugin .btn.btn-rose.active.focus,.fixed-plugin .btn.btn-rose.active:focus,.fixed-plugin .btn.btn-rose.active:hover,.fixed-plugin .btn.btn-rose:active.focus,.fixed-plugin .btn.btn-rose:active:focus,.fixed-plugin .btn.btn-rose:active:hover,.open>.fixed-plugin .btn.btn-rose.dropdown-toggle.focus,.open>.fixed-plugin .btn.btn-rose.dropdown-toggle:focus,.open>.fixed-plugin .btn.btn-rose.dropdown-toggle:hover,.show>.fixed-plugin .btn.btn-rose.dropdown-toggle.focus,.show>.fixed-plugin .btn.btn-rose.dropdown-toggle:focus,.show>.fixed-plugin .btn.btn-rose.dropdown-toggle:hover{color:#fff;background-color:#ea2c6d;border-color:#7b0c32}.open>.fixed-plugin .btn.btn-rose.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#e91e63}.open>.fixed-plugin .btn.btn-rose.dropdown-toggle.bmd-btn-icon:hover{background-color:#ea2c6d}.fixed-plugin .btn.btn-rose.disabled.focus,.fixed-plugin .btn.btn-rose.disabled:focus,.fixed-plugin .btn.btn-rose.disabled:hover,.fixed-plugin .btn.btn-rose:disabled.focus,.fixed-plugin .btn.btn-rose:disabled:focus,.fixed-plugin .btn.btn-rose:disabled:hover{background-color:#e91e63;border-color:#e91e63}.fixed-plugin .btn.btn-rose:active,.fixed-plugin .btn.btn-rose:focus,.fixed-plugin .btn.btn-rose:hover{box-shadow:0 14px 26px -12px rgba(233,30,99,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(233,30,99,.2)}.fixed-plugin .btn.btn-rose.btn-link{box-shadow:none}.fixed-plugin .btn.btn-rose.btn-link,.fixed-plugin .btn.btn-rose.btn-link:active,.fixed-plugin .btn.btn-rose.btn-link:focus,.fixed-plugin .btn.btn-rose.btn-link:hover{background-color:transparent;color:#e91e63}.fixed-plugin .btn,.fixed-plugin .btn.btn-default{color:#fff;background-color:#999;border-color:#999;box-shadow:0 2px 2px 0 hsla(0,0%,60%,.14),0 3px 1px -2px hsla(0,0%,60%,.2),0 1px 5px 0 hsla(0,0%,60%,.12)}.fixed-plugin .btn.btn-default.focus,.fixed-plugin .btn.btn-default:focus,.fixed-plugin .btn.btn-default:hover,.fixed-plugin .btn.focus,.fixed-plugin .btn:focus,.fixed-plugin .btn:hover{color:#fff;background-color:#919191;border-color:#7a7a7a}.fixed-plugin .btn.active,.fixed-plugin .btn.btn-default.active,.fixed-plugin .btn.btn-default:active,.fixed-plugin .btn:active,.open>.fixed-plugin .btn.btn-default.dropdown-toggle,.open>.fixed-plugin .btn.dropdown-toggle,.show>.fixed-plugin .btn.btn-default.dropdown-toggle,.show>.fixed-plugin .btn.dropdown-toggle{color:#fff;background-color:#919191;border-color:#7a7a7a;box-shadow:0 2px 2px 0 hsla(0,0%,60%,.14),0 3px 1px -2px hsla(0,0%,60%,.2),0 1px 5px 0 hsla(0,0%,60%,.12)}.fixed-plugin .btn.active.focus,.fixed-plugin .btn.active:focus,.fixed-plugin .btn.active:hover,.fixed-plugin .btn.btn-default.active.focus,.fixed-plugin .btn.btn-default.active:focus,.fixed-plugin .btn.btn-default.active:hover,.fixed-plugin .btn.btn-default:active.focus,.fixed-plugin .btn.btn-default:active:focus,.fixed-plugin .btn.btn-default:active:hover,.fixed-plugin .btn:active.focus,.fixed-plugin .btn:active:focus,.fixed-plugin .btn:active:hover,.open>.fixed-plugin .btn.btn-default.dropdown-toggle.focus,.open>.fixed-plugin .btn.btn-default.dropdown-toggle:focus,.open>.fixed-plugin .btn.btn-default.dropdown-toggle:hover,.open>.fixed-plugin .btn.dropdown-toggle.focus,.open>.fixed-plugin .btn.dropdown-toggle:focus,.open>.fixed-plugin .btn.dropdown-toggle:hover,.show>.fixed-plugin .btn.btn-default.dropdown-toggle.focus,.show>.fixed-plugin .btn.btn-default.dropdown-toggle:focus,.show>.fixed-plugin .btn.btn-default.dropdown-toggle:hover,.show>.fixed-plugin .btn.dropdown-toggle.focus,.show>.fixed-plugin .btn.dropdown-toggle:focus,.show>.fixed-plugin .btn.dropdown-toggle:hover{color:#fff;background-color:#919191;border-color:#595959}.open>.fixed-plugin .btn.btn-default.dropdown-toggle.bmd-btn-icon,.open>.fixed-plugin .btn.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#999}.open>.fixed-plugin .btn.btn-default.dropdown-toggle.bmd-btn-icon:hover,.open>.fixed-plugin .btn.dropdown-toggle.bmd-btn-icon:hover{background-color:#919191}.fixed-plugin .btn.btn-default.disabled.focus,.fixed-plugin .btn.btn-default.disabled:focus,.fixed-plugin .btn.btn-default.disabled:hover,.fixed-plugin .btn.btn-default:disabled.focus,.fixed-plugin .btn.btn-default:disabled:focus,.fixed-plugin .btn.btn-default:disabled:hover,.fixed-plugin .btn.disabled.focus,.fixed-plugin .btn.disabled:focus,.fixed-plugin .btn.disabled:hover,.fixed-plugin .btn:disabled.focus,.fixed-plugin .btn:disabled:focus,.fixed-plugin .btn:disabled:hover{background-color:#999;border-color:#999}.fixed-plugin .btn.btn-default:active,.fixed-plugin .btn.btn-default:focus,.fixed-plugin .btn.btn-default:hover,.fixed-plugin .btn:active,.fixed-plugin .btn:focus,.fixed-plugin .btn:hover{box-shadow:0 14px 26px -12px hsla(0,0%,60%,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px hsla(0,0%,60%,.2)}.fixed-plugin .btn.btn-default.btn-link,.fixed-plugin .btn.btn-link{background-color:transparent;color:#999;box-shadow:none}.fixed-plugin .btn.btn-default.btn-link:active,.fixed-plugin .btn.btn-default.btn-link:focus,.fixed-plugin .btn.btn-default.btn-link:hover,.fixed-plugin .btn.btn-link:active,.fixed-plugin .btn.btn-link:focus,.fixed-plugin .btn.btn-link:hover{background-color:transparent;color:#999}.fixed-plugin .btn.active.focus,.fixed-plugin .btn.active:focus,.fixed-plugin .btn.focus,.fixed-plugin .btn:active.focus,.fixed-plugin .btn:active:focus,.fixed-plugin .btn:focus{outline:0}.fixed-plugin .btn.btn-round{border-radius:30px}.fixed-plugin .button-container .btn:not(.btn-facebook):not(.btn-twitter){display:block}.fixed-plugin .button-container.github-star{margin-left:78px}.fixed-plugin #sharrreTitle{text-align:center;padding:10px 0;height:50px}.fixed-plugin li.header-title{height:30px;line-height:25px;font-size:12px;font-weight:600;text-transform:uppercase;text-align:center}.fixed-plugin .adjustments-line p{float:left;display:inline-block;margin-bottom:0;font-size:1em;color:#3c4858;padding-top:0}.fixed-plugin .adjustments-line a .badge-colors{position:relative;top:-2px}.fixed-plugin .adjustments-line .togglebutton{padding-right:7px}.fixed-plugin .adjustments-line .togglebutton .toggle{margin-right:0}.fixed-plugin .dropdown-menu>li.adjustments-line>a{padding-right:0;padding-left:0;border-radius:0;margin:0}.fixed-plugin .dropdown-menu>li>a.img-holder{font-size:16px;text-align:center;border-radius:10px;background-color:#fff;border:3px solid #fff;padding-left:0;padding-right:0;opacity:1;cursor:pointer;display:block;max-height:100px;overflow:hidden;padding:0;min-width:25%}.fixed-plugin .dropdown-menu>li>a.switch-trigger:focus,.fixed-plugin .dropdown-menu>li>a.switch-trigger:hover{background-color:transparent}.fixed-plugin .dropdown-menu>li:focus>a.img-holder,.fixed-plugin .dropdown-menu>li:hover>a.img-holder{border-color:rgba(0,187,255,.53)}.fixed-plugin .dropdown-menu>.active>a.img-holder{border-color:#0bf;background-color:#fff}.fixed-plugin .dropdown-menu>li>a img{margin-top:auto}.fixed-plugin .btn-social{width:50%;display:block;width:48%;float:left;font-weight:600}.fixed-plugin .btn-social i{margin-right:5px}.fixed-plugin .btn-social:first-child{margin-right:2%}.fixed-plugin .adjustments-line a,.fixed-plugin .adjustments-line a:focus,.fixed-plugin .adjustments-line a:hover{color:transparent}.fixed-plugin .dropdown .dropdown-menu{top:-40px!important;opacity:0;left:-303px!important;transform-origin:100% 0}.fixed-plugin .dropdown.show .dropdown-menu{opacity:1;transform:scale(1)}.fixed-plugin .dropdown-menu:after,.fixed-plugin .dropdown-menu:before{content:"";display:inline-block;position:absolute;top:65px;width:16px;transform:translateY(-50%);-webkit-transform:translateY(-50%);-moz-transform:translateY(-50%)}.fixed-plugin .dropdown-menu:before{border-bottom:16px solid transparent;border-left:16px solid rgba(0,0,0,.2);border-top:16px solid transparent;right:-16px}.fixed-plugin .dropdown-menu:after{border-bottom:16px solid transparent;border-left:16px solid #fff;border-top:16px solid transparent;right:-15px}.wrapper-full-page~.fixed-plugin .dropdown.open .dropdown-menu{transform:translateY(-17%)}.wrapper-full-page~.fixed-plugin .dropdown .dropdown-menu{transform:translateY(-19%)}.table>thead>tr>th{border-bottom-width:1px;font-size:1.0625rem;font-weight:300}.table .form-check{margin-top:0}.table .form-check .form-check-sign{top:-13px;left:0;padding-right:0}.table .checkbox,.table .radio{margin-top:0;margin-bottom:0;padding:0;width:15px}.table .checkbox .icons,.table .radio .icons{position:relative}.table .flag img{max-width:18px;margin-top:-2px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:12px 8px;vertical-align:middle;border-color:#ddd}.table thead tr th{font-size:1.063rem}.table .th-description{max-width:150px}.table .td-price{font-size:26px;font-weight:300;margin-top:5px;text-align:right}.table .td-total{font-weight:500;font-size:1.0625rem;padding-top:20px;text-align:right}.table .td-actions .btn{margin:0;padding:5px}.table>tbody>tr{position:relative}.table-shopping>thead>tr>th{font-size:.75rem;text-transform:uppercase}.table-shopping>tbody>tr>td{font-size:14px}.table-shopping>tbody>tr>td b{display:block;margin-bottom:5px}.table-shopping .td-name{font-weight:400;font-size:1.5em;line-height:1.42857143}.table-shopping .td-name small{color:#999;font-size:.75em;font-weight:300}.table-shopping .td-number{font-weight:300;font-size:1.125rem}.table-shopping .td-name{min-width:200px}.table-shopping .td-number{text-align:right;min-width:150px}.table-shopping .td-number small{margin-right:3px}.table-shopping .img-container{width:120px;max-height:160px;overflow:hidden;display:block}.table-shopping .img-container img{width:100%}.table-inverse{color:hsla(0,0%,100%,.84)}.table thead th{font-size:.95rem;font-weight:500;border-top-width:0;border-bottom-width:1px}.table-inverse thead th,thead.thead-inverse th{color:hsla(0,0%,100%,.54)}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:hsla(0,0%,100%,.06)}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table.table-hover tbody tr:hover{background-color:#f5f5f5}.dataTable>tbody>tr>td,.dataTable>tbody>tr>th,.dataTable>tfoot>tr>td,.dataTable>tfoot>tr>th,.dataTable>thead>tr>td,.dataTable>thead>tr>th{padding:5px!important}body{background-color:#eee;color:#3c4858;font-weight:300}legend{border-bottom:0}.serif-font{font-family:Roboto Slab,Times New Roman,serif}*{-webkit-tap-highlight-color:rgba(255,255,255,0);-webkit-tap-highlight-color:transparent}:focus{outline:0}a{color:#9c27b0}a:focus,a:hover{color:#89229b;text-decoration:none}a.text-info:focus,a.text-info:hover{color:#00a5bb}a .material-icons{vertical-align:middle}.form-check,label{font-size:14px;line-height:1.42857;color:#aaa;font-weight:400}.animation-transition-general,.sidebar .nav p,.sidebar .sidebar-wrapper .user .user-info [data-toggle=collapse]~div>ul>li>a span,.sidebar .sidebar-wrapper>.nav [data-toggle=collapse]~div>ul>li>a span{transition:all .3s linear}.animation-transition-slow{transition:all .37s linear}.animation-transition-fast{transition:all .15s ease 0s}.caret,.sidebar a{transition:all .15s ease-in}.offline-doc .navbar.navbar-transparent{padding-top:25px;border-bottom:none}.offline-doc .navbar.navbar-transparent .navbar-minimize{display:none}.offline-doc .navbar.navbar-transparent .collapse .navbar-nav .nav-link,.offline-doc .navbar.navbar-transparent .navbar-brand{color:#fff!important}.offline-doc .footer{z-index:3!important;position:absolute;width:100%;background:transparent;bottom:0;color:#fff}.offline-doc .page-header{display:flex;align-items:center}.offline-doc .page-header .content-center{z-index:3}.offline-doc .page-header .content-center .brand .title{color:#fff}.offline-doc .page-header:after{background-color:rgba(0,0,0,.5);content:"";display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:2}.bd-docs .bd-toc-item .bd-sidenav a span{float:right;margin-top:5px;padding:3px 7px;font-size:8px;line-height:9px;background-color:#9c27b0}.bootstrap-datetimepicker-widget .timepicker .table-condesed .btn .ripple-container{width:40px;height:40px;margin:-11px 3px}.off-canvas-sidebar .wrapper-full-page .page-header{padding:15vh 0!important}html[dir=rtl] .main-panel{float:left}html[dir=rtl] .off-canvas-sidebar nav .navbar-collapse,html[dir=rtl] .sidebar{text-align:right}html[dir=rtl] .sidebar{left:unset;right:0}html[dir=rtl] .sidebar .nav{padding-right:0}html[dir=rtl] .sidebar .nav i{float:right;margin-left:15px;margin-right:unset}html[dir=rtl] .card.card-chart{direction:ltr}html[dir=rtl] .card.card-chart .card-category,html[dir=rtl] .card.card-chart .card-title{text-align:right}html[dir=rtl] .card .card-body,html[dir=rtl] .card .card-footer{direction:rtl}html[dir=rtl] .form-check .form-check-sign .check:before{margin-right:10px}.btn.btn-facebook{color:#fff;background-color:#3b5998;border-color:#3b5998;box-shadow:0 2px 2px 0 rgba(59,89,152,.14),0 3px 1px -2px rgba(59,89,152,.2),0 1px 5px 0 rgba(59,89,152,.12)}.btn.btn-facebook.focus,.btn.btn-facebook:focus,.btn.btn-facebook:hover{color:#fff;background-color:#37538d;border-color:#2a3f6c}.btn.btn-facebook.active,.btn.btn-facebook:active,.open>.btn.btn-facebook.dropdown-toggle,.show>.btn.btn-facebook.dropdown-toggle{color:#fff;background-color:#37538d;border-color:#2a3f6c;box-shadow:0 2px 2px 0 rgba(59,89,152,.14),0 3px 1px -2px rgba(59,89,152,.2),0 1px 5px 0 rgba(59,89,152,.12)}.btn.btn-facebook.active.focus,.btn.btn-facebook.active:focus,.btn.btn-facebook.active:hover,.btn.btn-facebook:active.focus,.btn.btn-facebook:active:focus,.btn.btn-facebook:active:hover,.open>.btn.btn-facebook.dropdown-toggle.focus,.open>.btn.btn-facebook.dropdown-toggle:focus,.open>.btn.btn-facebook.dropdown-toggle:hover,.show>.btn.btn-facebook.dropdown-toggle.focus,.show>.btn.btn-facebook.dropdown-toggle:focus,.show>.btn.btn-facebook.dropdown-toggle:hover{color:#fff;background-color:#37538d;border-color:#17233c}.open>.btn.btn-facebook.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#3b5998}.open>.btn.btn-facebook.dropdown-toggle.bmd-btn-icon:hover{background-color:#37538d}.btn.btn-facebook.disabled.focus,.btn.btn-facebook.disabled:focus,.btn.btn-facebook.disabled:hover,.btn.btn-facebook:disabled.focus,.btn.btn-facebook:disabled:focus,.btn.btn-facebook:disabled:hover{background-color:#3b5998;border-color:#3b5998}.btn.btn-facebook:active,.btn.btn-facebook:focus,.btn.btn-facebook:hover{box-shadow:0 14px 26px -12px rgba(59,89,152,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(59,89,152,.2)}.btn.btn-facebook.btn-link{box-shadow:none}.btn.btn-facebook.btn-link,.btn.btn-facebook.btn-link:active,.btn.btn-facebook.btn-link:focus,.btn.btn-facebook.btn-link:hover{background-color:transparent;color:#3b5998}.btn.btn-twitter{color:#fff;background-color:#55acee;border-color:#55acee;box-shadow:0 2px 2px 0 rgba(85,172,238,.14),0 3px 1px -2px rgba(85,172,238,.2),0 1px 5px 0 rgba(85,172,238,.12)}.btn.btn-twitter.focus,.btn.btn-twitter:focus,.btn.btn-twitter:hover{color:#fff;background-color:#47a5ed;border-color:#1d91e8}.btn.btn-twitter.active,.btn.btn-twitter:active,.open>.btn.btn-twitter.dropdown-toggle,.show>.btn.btn-twitter.dropdown-toggle{color:#fff;background-color:#47a5ed;border-color:#1d91e8;box-shadow:0 2px 2px 0 rgba(85,172,238,.14),0 3px 1px -2px rgba(85,172,238,.2),0 1px 5px 0 rgba(85,172,238,.12)}.btn.btn-twitter.active.focus,.btn.btn-twitter.active:focus,.btn.btn-twitter.active:hover,.btn.btn-twitter:active.focus,.btn.btn-twitter:active:focus,.btn.btn-twitter:active:hover,.open>.btn.btn-twitter.dropdown-toggle.focus,.open>.btn.btn-twitter.dropdown-toggle:focus,.open>.btn.btn-twitter.dropdown-toggle:hover,.show>.btn.btn-twitter.dropdown-toggle.focus,.show>.btn.btn-twitter.dropdown-toggle:focus,.show>.btn.btn-twitter.dropdown-toggle:hover{color:#fff;background-color:#47a5ed;border-color:#126db2}.open>.btn.btn-twitter.dropdown-toggle.bmd-btn-icon{color:inherit;background-color:#55acee}.open>.btn.btn-twitter.dropdown-toggle.bmd-btn-icon:hover{background-color:#47a5ed}.btn.btn-twitter.disabled.focus,.btn.btn-twitter.disabled:focus,.btn.btn-twitter.disabled:hover,.btn.btn-twitter:disabled.focus,.btn.btn-twitter:disabled:focus,.btn.btn-twitter:disabled:hover{background-color:#55acee;border-color:#55acee}.btn.btn-twitter:active,.btn.btn-twitter:focus,.btn.btn-twitter:hover{box-shadow:0 14px 26px -12px rgba(85,172,238,.42),0 4px 23px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(85,172,238,.2)}.btn.btn-twitter.btn-link{box-shadow:none}.btn.btn-twitter.btn-link,.btn.btn-twitter.btn-link:active,.btn.btn-twitter.btn-link:focus,.btn.btn-twitter.btn-link:hover{background-color:transparent;color:#55acee}.card{border:0;margin-bottom:30px;margin-top:30px;border-radius:6px;color:#333;background:#fff;width:100%;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.card .card-category:not([class*=text-]){color:#999}.card .card-category{margin-top:10px}.card .card-category .material-icons{position:relative;top:8px;line-height:0}.card .form-check{margin-top:5px}.card .card-title{margin-top:.625rem}.card .card-title:last-child{margin-bottom:0}.card.no-shadow .card-header-image,.card.no-shadow .card-header-image img{box-shadow:none!important}.card .card-body,.card .card-footer{padding:.9375rem 1.875rem}.card .card-body+.card-footer{padding-top:0;border:0;border-radius:6px}.card .card-footer{display:flex;align-items:center;background-color:transparent;border:0}.card .card-footer .author,.card .card-footer .stats{display:inline-flex}.card .card-footer .stats{color:#999}.card .card-footer .stats .material-icons{position:relative;top:-10px;margin-right:3px;margin-left:3px;font-size:18px}.card.bmd-card-raised{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}@media (min-width:992px){.card.bmd-card-flat{box-shadow:none}}.card .card-header{border-bottom:none;background:transparent}.card .card-header .title{color:#fff}.card .card-header .nav-tabs{padding:0}.card .card-header.card-header-image{position:relative;padding:0;z-index:1;margin-left:15px;margin-right:15px;margin-top:-30px;border-radius:6px}.card .card-header.card-header-image img{width:100%;border-radius:6px;pointer-events:none;box-shadow:0 5px 15px -8px rgba(0,0,0,.24),0 8px 10px -5px rgba(0,0,0,.2)}.card .card-header.card-header-image .card-title{position:absolute;bottom:15px;left:15px;color:#fff;font-size:1.125rem;text-shadow:0 2px 5px rgba(33,33,33,.5)}.card .card-header.card-header-image .colored-shadow{transform:scale(.94);top:12px;filter:blur(12px);position:absolute;width:100%;height:100%;background-size:cover;z-index:-1;transition:opacity .45s;opacity:0}.card .card-header.card-header-image.no-shadow{box-shadow:none}.card .card-header.card-header-image.no-shadow.shadow-normal{box-shadow:0 16px 38px -12px rgba(0,0,0,.56),0 4px 25px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.card .card-header.card-header-image.no-shadow .colored-shadow{display:none!important}.card.bg-primary,.card .card-header-primary .card-icon,.card .card-header-primary .card-text,.card .card-header-primary:not(.card-header-icon):not(.card-header-text),.card.card-rotate.bg-primary .back,.card.card-rotate.bg-primary .front{background:linear-gradient(60deg,#ab47bc,#8e24aa)}.card.bg-info,.card .card-header-info .card-icon,.card .card-header-info .card-text,.card .card-header-info:not(.card-header-icon):not(.card-header-text),.card.card-rotate.bg-info .back,.card.card-rotate.bg-info .front{background:linear-gradient(60deg,#26c6da,#00acc1)}.card.bg-success,.card .card-header-success .card-icon,.card .card-header-success .card-text,.card .card-header-success:not(.card-header-icon):not(.card-header-text),.card.card-rotate.bg-success .back,.card.card-rotate.bg-success .front{background:linear-gradient(60deg,#66bb6a,#43a047)}.card.bg-warning,.card .card-header-warning .card-icon,.card .card-header-warning .card-text,.card .card-header-warning:not(.card-header-icon):not(.card-header-text),.card.card-rotate.bg-warning .back,.card.card-rotate.bg-warning .front{background:linear-gradient(60deg,#ffa726,#fb8c00)}.card.bg-danger,.card .card-header-danger .card-icon,.card .card-header-danger .card-text,.card .card-header-danger:not(.card-header-icon):not(.card-header-text),.card.card-rotate.bg-danger .back,.card.card-rotate.bg-danger .front{background:linear-gradient(60deg,#ef5350,#e53935)}.card.bg-rose,.card .card-header-rose .card-icon,.card .card-header-rose .card-text,.card .card-header-rose:not(.card-header-icon):not(.card-header-text),.card.card-rotate.bg-rose .back,.card.card-rotate.bg-rose .front{background:linear-gradient(60deg,#ec407a,#d81b60)}.card .card-header-primary .card-icon,.card .card-header-primary .card-text,.card .card-header-primary:not(.card-header-icon):not(.card-header-text){box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(156,39,176,.4)}.card .card-header-danger .card-icon,.card .card-header-danger .card-text,.card .card-header-danger:not(.card-header-icon):not(.card-header-text){box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(244,67,54,.4)}.card .card-header-rose .card-icon,.card .card-header-rose .card-text,.card .card-header-rose:not(.card-header-icon):not(.card-header-text){box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(233,30,99,.4)}.card .card-header-warning .card-icon,.card .card-header-warning .card-text,.card .card-header-warning:not(.card-header-icon):not(.card-header-text){box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(255,152,0,.4)}.card .card-header-info .card-icon,.card .card-header-info .card-text,.card .card-header-info:not(.card-header-icon):not(.card-header-text){box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(0,188,212,.4)}.card .card-header-success .card-icon,.card .card-header-success .card-text,.card .card-header-success:not(.card-header-icon):not(.card-header-text){box-shadow:0 4px 20px 0 rgba(0,0,0,.14),0 7px 10px -5px rgba(76,175,80,.4)}.card[class*=bg-],.card[class*=bg-] .card-title,.card[class*=bg-] .card-title a,.card[class*=bg-] .icon i,.card [class*=card-header-],.card [class*=card-header-] .card-title,.card [class*=card-header-] .card-title a,.card [class*=card-header-] .icon i{color:#fff}.card[class*=bg-] .icon i,.card [class*=card-header-] .icon i{border-color:hsla(0,0%,100%,.25)}.card[class*=bg-] .author a,.card[class*=bg-] .card-category,.card[class*=bg-] .card-description,.card[class*=bg-] .stats,.card [class*=card-header-] .author a,.card [class*=card-header-] .card-category,.card [class*=card-header-] .card-description,.card [class*=card-header-] .stats{color:hsla(0,0%,100%,.8)}.card[class*=bg-] .author a:active,.card[class*=bg-] .author a:focus,.card[class*=bg-] .author a:hover,.card [class*=card-header-] .author a:active,.card [class*=card-header-] .author a:focus,.card [class*=card-header-] .author a:hover{color:#fff}.card .author .avatar{width:30px;height:30px;overflow:hidden;border-radius:50%;margin-right:5px}.card .author a{color:#3c4858;text-decoration:none}.card .author a .ripple-container{display:none}.card .card-category-social .fa{font-size:24px;position:relative;margin-top:-4px;top:2px;margin-right:5px}.card .card-category-social .material-icons{position:relative;top:5px}.card[class*=bg-],.card[class*=bg-] .card-body{border-radius:6px}.card[class*=bg-] .card-body h1 small,.card[class*=bg-] .card-body h2 small,.card[class*=bg-] .card-body h3 small,.card[class*=bg-] h1 small,.card[class*=bg-] h2 small,.card[class*=bg-] h3 small{color:hsla(0,0%,100%,.8)}.card .card-stats{background:transparent;display:flex}.card .card-stats .author,.card .card-stats .stats{display:inline-flex}.card{box-shadow:0 1px 4px 0 rgba(0,0,0,.14)}.card .table tr:first-child td{border-top:none}.card .card-title{margin-top:0;margin-bottom:3px}.card .card-body{padding:.9375rem 20px;position:relative}.card .card-body .form-group{margin:8px 0 0}.card .card-header{z-index:3!important}.card .card-header .card-title{margin-bottom:3px}.card .card-header .card-category{margin:0}.card .card-header.card-header-text{display:inline-block}.card .card-header.card-header-text:after{content:"";display:table}.card .card-header.card-header-icon i,.card .card-header.card-header-text i{width:33px;height:33px;text-align:center;line-height:33px}.card .card-header.card-header-icon .card-title,.card .card-header.card-header-text .card-title{margin-top:15px;color:#3c4858}.card .card-header.card-header-icon h4,.card .card-header.card-header-text h4{font-weight:300}.card .card-header.card-header-tabs .nav-tabs{background:transparent;padding:0}.card .card-header.card-header-tabs .nav-tabs-title{float:left;padding:10px 10px 10px 0;line-height:24px}.card.card-plain .card-header.card-header-icon+.card-body .card-category,.card.card-plain .card-header.card-header-icon+.card-body .card-title{margin-top:-20px}.card .card-actions{position:absolute;z-index:1;top:-50px;width:calc(100% - 30px);left:17px;right:17px;text-align:center}.card .card-actions .card-header{padding:0;min-height:160px}.card .card-actions .btn{padding-left:12px;padding-right:12px}.card .card-actions .fix-broken-card{position:absolute;top:-65px}.card.card-chart .card-footer i:nth-child(1n+2){width:18px;text-align:center}.card.card-chart .card-category{margin:0}.card .card-body+.card-footer,.card .card-footer{padding:0;padding-top:10px;margin:0 15px 10px;border-radius:0;justify-content:space-between;align-items:center}.card .card-body+.card-footer h6,.card .card-footer h6{width:100%}.card .card-body+.card-footer .stats,.card .card-footer .stats{color:#999;font-size:12px;line-height:22px}.card .card-body+.card-footer .stats .card-category,.card .card-footer .stats .card-category{padding-top:7px;padding-bottom:7px;margin:0}.card .card-body+.card-footer .stats .material-icons,.card .card-footer .stats .material-icons{position:relative;top:4px;font-size:16px}.card [class*=card-header-]{margin:0 15px;padding:0;position:relative}.card [class*=card-header-] .card-title+.card-category{color:hsla(0,0%,100%,.8)}.card [class*=card-header-] .card-title+.card-category a{color:#fff}.card [class*=card-header-]:not(.card-header-icon):not(.card-header-text):not(.card-header-image){border-radius:3px;margin-top:-20px;padding:15px}.card [class*=card-header-] .card-icon,.card [class*=card-header-] .card-text{border-radius:3px;background-color:#999;padding:15px;margin-top:-20px;margin-right:15px;float:left}.card [class*=card-header-] .card-text{float:none;display:inline-block;margin-right:0}.card [class*=card-header-] .card-text .card-title{color:#fff;margin-top:0}.card [class*=card-header-] .ct-chart .card-title{color:#fff}.card [class*=card-header-] .ct-chart .card-category{margin-bottom:0;color:hsla(0,0%,100%,.62)}.card [class*=card-header-] .ct-chart .ct-label{color:hsla(0,0%,100%,.7)}.card [class*=card-header-] .ct-chart .ct-grid{stroke:hsla(0,0%,100%,.2)}.card [class*=card-header-] .ct-chart .ct-series-a .ct-bar,.card [class*=card-header-] .ct-chart .ct-series-a .ct-line,.card [class*=card-header-] .ct-chart .ct-series-a .ct-point,.card [class*=card-header-] .ct-chart .ct-series-a .ct-slice-donut{stroke:hsla(0,0%,100%,.8)}.card [class*=card-header-] .ct-chart .ct-series-a .ct-area,.card [class*=card-header-] .ct-chart .ct-series-a .ct-slice-pie{fill:hsla(0,0%,100%,.4)}.card [class*=card-header-] .ct-chart .ct-series-a .ct-bar{stroke-width:10px}.card [class*=card-header-] .ct-chart .ct-point{stroke-width:10px;stroke-linecap:round}.card [class*=card-header-] .ct-chart .ct-line{fill:none;stroke-width:4px}.card [data-header-animation=true]{transform:translateZ(0);transition:all .3s cubic-bezier(.34,1.61,.7,1)}.card:hover [data-header-animation=true]{transform:translate3d(0,-50px,0)}.card .map{height:280px;border-radius:6px;margin-top:15px}.card .map.map-big{height:420px}.card .card-body.table-full-width{padding:0}.card .card-plain .card-header-icon{margin-right:15px!important}.table-sales{margin-top:40px}.iframe-container{width:100%}.iframe-container iframe{width:100%;height:500px;border:0;box-shadow:0 16px 38px -12px rgba(0,0,0,.56),0 4px 25px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.card-wizard .nav.nav-pills .nav-item{margin:0}.card-wizard .nav.nav-pills .nav-item .nav-link{padding:6px 15px!important}.card-wizard .nav-pills:not(.flex-column) .nav-item+.nav-item:not(:first-child){margin-left:0}.card-wizard .nav-item .nav-link.active,.card-wizard .nav-item .nav-link:focus,.card-wizard .nav-item .nav-link:hover{background-color:inherit!important;box-shadow:none!important}.card-wizard .input-group-text{padding:6px 15px 0!important}.card-wizard .card-footer{border-top:none!important}.card-chart .card-body+.card-footer,.card-product .card-body+.card-footer{border-top:1px solid #eee}.card-product .price{color:inherit}.card-collapse{margin-bottom:15px}.card-collapse .card .card-header a[aria-expanded=true]{color:#e91e63}.card-stats .card-header.card-header-icon,.card-stats .card-header.card-header-text{text-align:right}.card-stats .card-header .card-icon+.card-category,.card-stats .card-header .card-icon+.card-title{padding-top:10px}.card-stats .card-header.card-header-icon .card-category,.card-stats .card-header.card-header-icon .card-title,.card-stats .card-header.card-header-text .card-category,.card-stats .card-header.card-header-text .card-title{margin:0}.card-stats .card-header .card-category{margin-bottom:0;margin-top:0}.card-stats .card-header .card-category:not([class*=text-]){color:#999;font-size:14px}.card-stats .card-header+.card-footer{border-top:1px solid #eee;margin-top:20px}.card-stats .card-header.card-header-icon i{font-size:36px;line-height:56px;width:56px;height:56px;text-align:center}.card-stats .card-body{text-align:right}.card-profile{margin-top:30px;text-align:center}.card-profile .card-avatar{margin:-50px auto 0;border-radius:50%;overflow:hidden;padding:0;box-shadow:0 16px 38px -12px rgba(0,0,0,.56),0 4px 25px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.card-profile .card-avatar+.card-body{margin-top:15px}.card-profile .card-avatar img{width:100%;height:auto}.card-profile .card-body+.card-footer{margin-top:-15px}.card-profile .card-footer .btn.btn-just-icon{font-size:20px;padding:12px;line-height:1em}.card-profile.card-plain .card-avatar{margin-top:0}.card-profile .card-header:not([class*=card-header-]){background:transparent}.card-profile .card-avatar{max-width:130px;max-height:130px}.card-plain{background:transparent;box-shadow:none}.card-plain .card-header:not(.card-avatar){margin-left:0;margin-right:0}.card-plain .card-body{padding-left:5px;padding-right:5px}.card-plain .card-header-image{margin:0!important;border-radius:6px}.card-plain .card-header-image img{border-radius:6px}.card-plain .card-footer{padding-left:5px;padding-right:5px;background-color:transparent}.animated{animation-duration:1s;animation-fill-mode:both}.animated.infinite{animation-iteration-count:infinite}.animated.hinge{animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{animation-duration:.75s}@keyframes e{0%,to{transform:translateZ(0)}10%,30%,50%,70%,90%{transform:translate3d(-10px,0,0)}20%,40%,60%,80%{transform:translate3d(10px,0,0)}}.shake{animation-name:e}@keyframes f{0%{opacity:0;transform:translate3d(0,-100%,0)}to{opacity:1;transform:none}}.fadeInDown{animation-name:f}@keyframes g{0%{opacity:1}to{opacity:0}}.fadeOut{animation-name:g}@keyframes h{0%{opacity:1}to{opacity:0;transform:translate3d(0,100%,0)}}.fadeOutDown{animation-name:h}@keyframes i{0%{opacity:1}to{opacity:0;transform:translate3d(0,-100%,0)}}.fadeOutUp{animation-name:i}.ct-chart .ct-series-a .ct-area,.ct-chart .ct-series-a .ct-bar,.ct-chart .ct-series-a .ct-line,.ct-chart .ct-series-a .ct-point,.ct-chart .ct-series-a .ct-slice-donut,.ct-chart .ct-series-a .ct-slice-donut-solid,.ct-chart .ct-series-a .ct-slice-pie{stroke:#00bcd4}.ct-chart .ct-series-b .ct-area,.ct-chart .ct-series-b .ct-bar,.ct-chart .ct-series-b .ct-line,.ct-chart .ct-series-b .ct-point,.ct-chart .ct-series-b .ct-slice-donut,.ct-chart .ct-series-b .ct-slice-donut-solid,.ct-chart .ct-series-b .ct-slice-pie{stroke:#f44336}.ct-chart .ct-series-c .ct-area,.ct-chart .ct-series-c .ct-bar,.ct-chart .ct-series-c .ct-line,.ct-chart .ct-series-c .ct-point,.ct-chart .ct-series-c .ct-slice-donut,.ct-chart .ct-series-c .ct-slice-donut-solid,.ct-chart .ct-series-c .ct-slice-pie{stroke:#ff9800}.ct-chart .ct-bar{fill:none;stroke-width:10px}.ct-chart .ct-line{fill:none;stroke-width:4px}.ct-chart .ct-point{stroke-width:10px;stroke-linecap:round}.ct-chart .ct-grid{stroke:rgba(0,0,0,.2);stroke-width:1px;stroke-dasharray:2px}.ct-chart .ct-label{fill:rgba(0,0,0,.4);color:rgba(0,0,0,.4);display:flex}.ct-chart .ct-label.ct-vertical.ct-start{-ms-flex-align:flex-end;align-items:flex-end;-ms-flex-pack:flex-end;justify-content:flex-end;text-align:right;text-anchor:end}.ct-chart .ct-series-a .ct-area,.ct-chart .ct-series-a .ct-slice-donut-solid,.ct-chart .ct-series-a .ct-slice-pie{fill:#00bcd4}.ct-chart .ct-series-b .ct-area,.ct-chart .ct-series-b .ct-slice-donut-solid,.ct-chart .ct-series-b .ct-slice-pie{fill:#f44336}.ct-chart .ct-series-c .ct-area,.ct-chart .ct-series-c .ct-slice-donut-solid,.ct-chart .ct-series-c .ct-slice-pie{fill:#ff9800}.ps-container{-ms-touch-action:auto;touch-action:auto;overflow:hidden!important;-ms-overflow-style:none}@supports (-ms-overflow-style:none){.ps-container{overflow:auto!important}}@media (-ms-high-contrast:none),screen and (-ms-high-contrast:active){.ps-container{overflow:auto!important}}.ps-container.ps-active-x>.ps-scrollbar-x-rail,.ps-container.ps-active-y>.ps-scrollbar-y-rail{display:block;background-color:transparent}.ps-container.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail{background-color:#eee;opacity:.9}.ps-container.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail>.ps-scrollbar-x{background-color:#999;height:11px}.ps-container.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail{background-color:#eee;opacity:.9}.ps-container.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail>.ps-scrollbar-y{background-color:#999;width:11px}.ps-container>.ps-scrollbar-x-rail{display:none;position:absolute;opacity:0;transition:background-color .2s linear,opacity .2s linear;bottom:0;height:15px}.ps-container>.ps-scrollbar-x-rail>.ps-scrollbar-x{position:absolute;background-color:#aaa;border-radius:6px;transition:background-color .2s linear,height .2s linear,width .2s ease-in-out,border-radius .2s ease-in-out;bottom:2px;height:6px}.ps-container>.ps-scrollbar-x-rail:active>.ps-scrollbar-x,.ps-container>.ps-scrollbar-x-rail:hover>.ps-scrollbar-x{height:11px}.ps-container>.ps-scrollbar-y-rail{display:none;position:absolute;opacity:0;transition:background-color .2s linear,opacity .2s linear;right:0;width:15px}.ps-container>.ps-scrollbar-y-rail>.ps-scrollbar-y{position:absolute;background-color:#aaa;border-radius:6px;transition:background-color .2s linear,height .2s linear,width .2s ease-in-out,border-radius .2s ease-in-out;right:2px;width:6px}.ps-container>.ps-scrollbar-y-rail:active>.ps-scrollbar-y,.ps-container>.ps-scrollbar-y-rail:hover>.ps-scrollbar-y{width:11px}.ps-container:hover.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail{background-color:#eee;opacity:.9}.ps-container:hover.ps-in-scrolling.ps-x>.ps-scrollbar-x-rail>.ps-scrollbar-x{background-color:#999;height:11px}.ps-container:hover.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail{background-color:#eee;opacity:.9}.ps-container:hover.ps-in-scrolling.ps-y>.ps-scrollbar-y-rail>.ps-scrollbar-y{background-color:#999;width:11px}.ps-container:hover>.ps-scrollbar-x-rail,.ps-container:hover>.ps-scrollbar-y-rail{opacity:.6}.ps-container:hover>.ps-scrollbar-x-rail:hover{background-color:#eee;opacity:.9}.ps-container:hover>.ps-scrollbar-x-rail:hover>.ps-scrollbar-x{background-color:#999}.ps-container:hover>.ps-scrollbar-y-rail:hover{background-color:#eee;opacity:.9}.ps-container:hover>.ps-scrollbar-y-rail:hover>.ps-scrollbar-y{background-color:#999}@media (max-width:991px){[class*=navbar-expand-]>.container,[class*=navbar-expand-]>.container-fluid{padding-left:15px;padding-right:15px}.navbar .navbar-collapse .navbar-nav>li.button-container{padding:15px}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:-webkit-fill-available!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) .dropdown-menu.show{min-width:auto;left:auto}.carousel .card .card-body{max-width:340px;margin:0 auto;min-height:400px}.navbar-collapse{position:fixed;display:block;top:0;height:100vh;width:230px;right:0;margin-right:0!important;z-index:1032;visibility:visible;background-color:#999;overflow-y:visible;border-top:none;text-align:left;padding-right:0;padding-left:0;max-height:none!important;transform:translate3d(230px,0,0);transition:all .5s cubic-bezier(.685,.0473,.346,1)}.navbar-collapse:after{top:0;left:0;height:100%;width:100%;position:absolute;background-color:#fff;display:block;content:"";z-index:1}.navbar-collapse .dropdown-toggle:after{position:absolute;right:16px;margin-top:8px}.navbar-collapse .navbar-nav{position:relative;z-index:3}.navbar-collapse .navbar-nav .nav-item .nav-link{color:#3c4858;margin:5px 15px}.navbar-collapse .navbar-nav .nav-item.button-container .nav-link{margin:15px}.navbar-collapse .navbar-nav .nav-item:after{width:calc(100% - 30px);content:"";display:block;height:1px;margin-left:15px}.navbar-collapse .navbar-nav .nav-item:last-child:after{display:none}.nav-open .navbar-collapse{transform:translateZ(0)}.nav-open .navbar-translate{transform:translate3d(-230px,0,0)}.navbar .navbar-translate{width:100%;position:relative;display:flex;-ms-flex-pack:justify!important;justify-content:space-between!important;-ms-flex-align:center;align-items:center;transition:transform .5s cubic-bezier(.685,.0473,.346,1)}.navbar .dropdown.show .dropdown-menu{display:block}.navbar .dropdown .dropdown-menu{display:none}.navbar .dropdown-menu .dropdown-item{margin-left:1.5rem;margin-right:1.5rem}.navbar .dropdown .dropdown-menu,.navbar .dropdown.show .dropdown-menu{background-color:transparent;border:0;padding-bottom:15px;transition:none;box-shadow:none;transform:none!important;width:auto;margin-bottom:15px;padding-top:0;height:300px;animation:none;opacity:1;overflow-y:scroll}.navbar.navbar-transparent .navbar-toggler .navbar-toggler-icon{background-color:#fff}#bodyClick{height:100%;width:100%;position:fixed;opacity:0;top:0;left:auto;right:230px;content:"";z-index:1029;overflow-x:hidden}#navbar .navbar-collapse,#navigation .navbar-collapse{display:none!important}.dropdown-menu.show .dropdown-item.open+.dropdown-menu.show{right:101%!important}.dropdown-menu.show .dropdown-item.open+.dropdown-menu.show .dropdown-item.open+.dropdown-menu,.dropdown-menu.show .dropdown-item.open+.dropdown-menu.show .dropdown-item.open+.dropdown-menu.show{left:-165px!important}}@media (min-width:991px){.navbar .navbar-nav{align-items:center}.navbar .navbar-nav .button-container{margin-left:.1875px}.sidebar .navbar-form{display:none!important}}@media screen and (max-width:991px){.presentation-page .section-components .components-macbook{max-width:850px!important;max-height:480px!important;margin-top:12vh;left:-12px}.presentation-page .section-components .coloured-card-img,.presentation-page .section-components .table-img{display:none}.presentation-page .section-components .social-img{left:47%;top:37%}.presentation-page .section-components .pin-btn-img{top:54%}.presentation-page .section-components .share-btn-img{top:12%}.presentation-page .section-components .coloured-card-btn-img{top:-2%;left:65%}.presentation-page .section-content .area-img{max-width:130px;max-height:170px}.presentation-page .section-content .info-img{max-width:170px;max-height:120px}}@media screen and (max-width:767px){.presentation-page .section-components .components-macbook{max-width:350px!important;max-height:250px!important;margin-top:12vh;left:-12px}.presentation-page .section-components .coloured-card-img,.presentation-page .section-components .table-img{display:none}.presentation-page .section-components .social-img{left:-7%;top:37%}.presentation-page .section-components .pin-btn-img{top:54%}.presentation-page .section-components .share-btn-img{top:7%}.presentation-page .section-components .coloured-card-btn-img{top:-2%}.login-page .container{padding-top:100px!important}.index-page #cd-vertical-nav,.presentation-page #cd-vertical-nav,.section-page #cd-vertical-nav{display:none}.index-page .cd-section .tim-typo .tim-note{width:60px}}@media screen and (max-width:400px){.cd-vertical-nav{display:none!important}}@media (max-width:991px){.form-group textarea{padding-top:15px}.nav-open .menu-on-left .main-panel{position:static}body,html{overflow-x:hidden}.nav-open .menu-on-left .main-panel,.nav-open .menu-on-left .navbar-fixed>div,.nav-open .menu-on-left .wrapper-full-page{transform:translate3d(260px,0,0)}.menu-on-left .off-canvas-sidebar,.menu-on-left .sidebar{left:0;right:auto;transform:translate3d(-260px,0,0)}.menu-on-left .close-layer{left:auto;right:0}.timeline:before,.timeline>li>.timeline-badge{left:5%}.timeline>li>.timeline-panel{float:right;width:86%}.timeline>li>.timeline-panel:before{border-left-width:0;border-right-width:15px;left:-15px;right:auto}.timeline>li>.timeline-panel:after{border-left-width:0;border-right-width:14px;left:-14px;right:auto}.nav-mobile-menu .dropdown .dropdown-menu{display:none;position:static!important;background-color:transparent;width:auto;float:none;box-shadow:none}.nav-mobile-menu .dropdown .dropdown-menu.showing{animation:initial;animation-duration:0s}.nav-mobile-menu .dropdown .dropdown-menu.hiding{transform:none;opacity:1}.nav-mobile-menu .dropdown.show .dropdown-menu{display:block}.nav-mobile-menu li.active>a{background-color:hsla(0,0%,100%,.1)}.navbar-minimize{display:none}.card .form-horizontal .label-on-left,.card .form-horizontal .label-on-right{padding-left:15px;padding-top:8px}.card .form-horizontal .form-group{margin-top:0}.card .form-horizontal .checkbox-radios{padding-bottom:15px}.card .form-horizontal .checkbox-inline,.card .form-horizontal .checkbox-radios .checkbox:first-child,.card .form-horizontal .checkbox-radios .radio:first-child{margin-top:0}.sidebar{display:none;box-shadow:none}.sidebar .sidebar-wrapper{padding-bottom:60px}.sidebar .nav-mobile-menu{margin-top:0}.sidebar .nav-mobile-menu .notification{float:left;line-height:30px;margin-right:8px}.sidebar .nav-mobile-menu .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.main-panel{width:100%}.navbar-transparent{padding-top:15px;background-color:rgba(0,0,0,.45)}body{position:relative}.nav-open .main-panel,.nav-open .navbar .container,.nav-open .navbar .container .navbar-toggler,.nav-open .navbar .container .navbar-wrapper,.nav-open .wrapper-full-page{left:0;transform:translate3d(-260px,0,0)}.nav-open .sidebar{box-shadow:0 16px 38px -12px rgba(0,0,0,.56),0 4px 25px 0 rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.nav-open .off-canvas-sidebar .navbar-collapse,.nav-open .sidebar{transform:translateZ(0)}.navbar .container,.navbar .container .navbar-toggler,.navbar .container .navbar-wrapper,.wrapper-full-page{transform:translateZ(0);transition:all .33s cubic-bezier(.685,.0473,.346,1);left:0}.off-canvas-sidebar .navbar .container{transform:none}.main-panel,.navbar-collapse{transition:all .33s cubic-bezier(.685,.0473,.346,1)}.navbar .navbar-collapse.collapse,.navbar .navbar-collapse.collapse.in,.navbar .navbar-collapse.collapsing{display:none!important}.off-canvas-sidebar .navbar .navbar-collapse.collapse,.off-canvas-sidebar .navbar .navbar-collapse.collapse.in,.off-canvas-sidebar .navbar .navbar-collapse.collapsing{display:block!important}.navbar-nav>li{float:none;position:relative;display:block}.off-canvas-sidebar nav .navbar-collapse{margin:0}.off-canvas-sidebar nav .navbar-collapse>ul{margin-top:19px}.off-canvas-sidebar nav .navbar-collapse,.sidebar{position:fixed;display:block;top:0;height:100vh;width:260px;right:0;left:auto;z-index:1032;visibility:visible;background-color:#9a9a9a;overflow-y:visible;border-top:none;text-align:left;padding-right:0;padding-left:0;transform:translate3d(260px,0,0);transition:all .33s cubic-bezier(.685,.0473,.346,1)}.off-canvas-sidebar nav .navbar-collapse>ul,.sidebar>ul{position:relative;z-index:4;width:100%}.off-canvas-sidebar nav .navbar-collapse:before,.sidebar:before{top:0;left:0;height:100%;width:100%;position:absolute;background-color:#282828;display:block;content:"";z-index:1}.off-canvas-sidebar nav .navbar-collapse .logo,.sidebar .logo{position:relative;z-index:4}.off-canvas-sidebar nav .navbar-collapse .navbar-form,.sidebar .navbar-form{margin:10px 0;float:none!important;padding-top:1px;padding-bottom:1px;position:relative}.off-canvas-sidebar nav .navbar-collapse .table-responsive,.sidebar .table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.form-group.form-search .form-control{font-size:1.7em;height:37px;width:78%}.navbar-form .btn{position:absolute;top:-5px;right:-50px}.close-layer{height:100%;width:100%;position:absolute;opacity:0;top:0;left:auto;background:rgba(0,0,0,.35);content:"";z-index:9999;overflow-x:hidden;transition:all .37s ease-in}.close-layer.visible{opacity:1}.navbar-toggler .icon-bar{display:block;position:relative;background:#555!important;width:24px;height:2px;border-radius:1px;margin:0 auto}.navbar-header .navbar-toggler{padding:15px;margin-top:4px;width:40px;height:40px}.bar1,.bar2,.bar3{outline:1px solid transparent}@keyframes j{0%{top:0;transform:rotate(0deg)}45%{top:6px;transform:rotate(145deg)}75%{transform:rotate(130deg)}to{transform:rotate(135deg)}}@keyframes k{0%{top:6px;transform:rotate(135deg)}45%{transform:rotate(-10deg)}75%{transform:rotate(5deg)}to{top:0;transform:rotate(0)}}@keyframes l{0%{bottom:0;transform:rotate(0deg)}45%{bottom:6px;transform:rotate(-145deg)}75%{transform:rotate(-130deg)}to{transform:rotate(-135deg)}}@keyframes m{0%{bottom:6px;transform:rotate(-135deg)}45%{transform:rotate(10deg)}75%{transform:rotate(-5deg)}to{bottom:0;transform:rotate(0)}}.navbar-toggler .icon-bar:nth-child(2){top:0;animation:k .5s 0s;animation-fill-mode:forwards}.navbar-toggler .icon-bar:nth-child(3){opacity:1}.navbar-toggler .icon-bar:nth-child(4){bottom:0;animation:m .5s 0s;animation-fill-mode:forwards}.navbar-toggler.toggled .icon-bar:nth-child(2){top:6px;animation:j .5s 0s;animation-fill-mode:forwards}.navbar-toggler.toggled .icon-bar:nth-child(3){opacity:0}.navbar-toggler.toggled .icon-bar:nth-child(4){bottom:6px;animation:l .5s 0s;animation-fill-mode:forwards}.dropdown-menu .divider{background-color:hsla(0,0%,90%,.15)}.navbar-nav{margin:1px 0}.navbar-nav .open .dropdown-menu>li>a{padding:15px 15px 5px 50px}.navbar-nav .open .dropdown-menu>li:first-child>a{padding:5px 15px 5px 50px}.navbar-nav .open .dropdown-menu>li:last-child>a{padding:15px 15px 25px 50px}[class*=navbar-] .navbar-nav .active>a,[class*=navbar-] .navbar-nav .active>a:focus,[class*=navbar-] .navbar-nav .active>a:hover,[class*=navbar-] .navbar-nav .navbar-nav .open .dropdown-menu>li>a:active,[class*=navbar-] .navbar-nav .open .dropdown-menu>li>a,[class*=navbar-] .navbar-nav .open .dropdown-menu>li>a:focus,[class*=navbar-] .navbar-nav .open .dropdown-menu>li>a:hover,[class*=navbar-] .navbar-nav>li>a,[class*=navbar-] .navbar-nav>li>a:focus,[class*=navbar-] .navbar-nav>li>a:hover{color:#fff}[class*=navbar-] .navbar-nav .open .dropdown-menu>li>a,[class*=navbar-] .navbar-nav .open .dropdown-menu>li>a:focus,[class*=navbar-] .navbar-nav .open .dropdown-menu>li>a:hover,[class*=navbar-] .navbar-nav>li>a,[class*=navbar-] .navbar-nav>li>a:focus,[class*=navbar-] .navbar-nav>li>a:hover{opacity:.7;background:transparent}[class*=navbar-] .navbar-nav.navbar-nav .open .dropdown-menu>li>a:active{opacity:1}[class*=navbar-] .navbar-nav .dropdown>a:hover .caret{border-bottom-color:#777;border-top-color:#777}[class*=navbar-] .navbar-nav .dropdown>a:active .caret{border-bottom-color:#fff;border-top-color:#fff}.dropdown-menu{display:none}.navbar-fixed-top{-webkit-backface-visibility:hidden}#bodyClick{height:100%;width:100%;position:fixed;opacity:0;top:0;left:auto;right:260px;content:"";z-index:9999;overflow-x:hidden}.social-line .btn,.subscribe-line .form-control{margin:0 0 10px}.footer:not(.footer-big) nav>ul li,.social-line.pull-right{float:none}.social-area.pull-right{float:none!important}.form-control+.form-control-feedback{margin-top:-8px}.navbar-toggle:focus,.navbar-toggle:hover{background-color:transparent!important}.media-post .author{width:20%;float:none!important;display:block;margin:0 auto 10px}.media-post .media-body{width:100%}.navbar-collapse.collapse{height:100%!important}.navbar-collapse.collapse.in{display:block}.navbar-header .collapse,.navbar-toggle{display:block!important}.navbar-header{float:none}.navbar-collapse .nav p{font-size:1rem;margin:0}}@media (min-width:992px){.main-panel .navbar .navbar-collapse .navbar-nav .nav-item .nav-link p{display:none}.nav-mobile-menu,.sidebar .navbar-form{display:none!important}} diff --git a/stlearn/app/static/css/style.css b/stlearn/app/static/css/style.css deleted file mode 100644 index 7f34cd22..00000000 --- a/stlearn/app/static/css/style.css +++ /dev/null @@ -1,95 +0,0 @@ -.container-fluid { - width: 80%; - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; -} - -.right { - float: right; - width: 300px; - border: 3px solid #73AD21; - padding: 10px; -} - -.card { - border: 0; - margin-bottom: 30px; - margin-top: 10px; - border-radius: 6px; - color: #333; - background: #fff; - width: 100%; - box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 20%), 0 1px 5px 0 rgb(0 0 0 / 12%); -} - -.main-panel>.content { - margin-top: 0px; - padding: 30px 15px; - min-height: calc(100vh - 123px); -} - -a.disabled, fieldset:disabled a { - pointer-events: none; - background-color: #D8D8D8; -} - - -.list-group-item { - position: relative; - display: block; - padding: 0.25rem 1.25rem; - margin-bottom: 0; - background-color: inherit; - border: 0 solid rgba(0,0,0,.125); -} - -#Cluster\ Select { - height: 150px -} - -#overlay { width:100px; - height: 100px; - position: fixed; - top: 50%; - left: 50%; - z-index: -1; -} - -#loading { width:100px; - height: 200px; - position: fixed; - top: 50%; - left: 50%; -} - -.btn, .btn.btn-default { - color: #fff; - background-color: #9124a3; - border-color: #9c27b0; - box-shadow: 0 2px 2px 0 hsl(0deg 0% 60% / 14%), 0 3px 1px -2px hsl(0deg 0% 60% / 20%), 0 1px 5px 0 hsl(0deg 0% 60% / 12%); -} - -.logging_window{ - display: block; - padding: 9.5px; - font-size: 13px; - line-height: 1.42857143; - color: #333; - word-break: break-all; - word-wrap: break-word; - background-color: #f5f5f5; - border: 1px solid #ccc; - border-radius: 4px; - width: 50%; - margin: auto; -} - -.sidebar { - width: 285px; -} - -.sidebar .sidebar-wrapper { - width: 285px; -} diff --git a/stlearn/app/static/img/Settings.gif b/stlearn/app/static/img/Settings.gif deleted file mode 100644 index 6c170a8db4ebc2faee1a0a85babd007634114a0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35587 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 T1QD=7vFOOhKKgMGfdBwI1ou8g diff --git a/stlearn/app/static/img/favicon.png b/stlearn/app/static/img/favicon.png deleted file mode 100644 index 621d986b0ff50d6a02a0da4a6c15ded93b7f9f43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 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 diff --git a/stlearn/app/static/img/loading_gif.gif b/stlearn/app/static/img/loading_gif.gif deleted file mode 100644 index 289a779cbb6197312ffe2f7a620cf67f280ea7c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29545 zcmeFac|6qn|Mx$;ow4uC*hxb6R5Oe8a_S7_sS#u01V{`7z=BQ}fdw=X<(Y^SQX5-nO(5|DK8Y`LFYB zcYE5#<|{MP=1L8sH@h~D&Ck7=9-f+?`#9g#ST|EUAuG z`Z~H;G<*p83yy~ewG^d|d?51ML=hI6vO{gPWsY{0gxw4_JIyW&x4+utv&YByK57$Ko)jyr7k6tjkkRsF&qxiF^P;~5Wt{6*+C9<;ehtduoIhvNdL?XdT z2$gd(H!uI>aoZE8ii=O%ik&GtDSy@mi@qdBs3BasTz3UqY0ZPdCZ7sd_ITAKxVqH4D-)EzV1nEcDRK3_IjJ?P1?`qvs6qMB>WW+vyh zuSgLLa}TIpzXe~h@wC=;Bg0CMy#crD>PK9jC2ilDw6NT3kLsE$gN&&*$`Kqheo#d2 z@}x_19Gj7(|@`OE$Ww45PHdCRj^X{8Hw1}pQ`V*I4!T=y22}Vyo`_bUVhn@kjax#2OXt$ zKIfi3De(F3`CIqT6LELX``o$VelhZ#b5SvISyr^`hqvd#A~c-1P=`<&BL{JU8YG5% za8--SX_RvkDVFGLt3Y(0v{qHmp_7>rlCCXQlJp$1pERkaFwsk+v}`O@+e{eu&q?{` zr2KPI{_&K5Jmnwy{C`Ft@85DtB80L0ipIu=(~U9O(-$4aANJ=bM)$X5HJ0W#R+Nmt znQzN>%`d2gRMs{z&kXX1Y*td&hJ?3?@sdm?>qb8Ws^Pmma|Loe4J?RZkle!E9eVc3 zjgJkN)U*vZME7@;wAHbYv^F&lX^oTRAgz^DG(sdBe==NhgxTMb-*~YE;$KN=C4@T2 ze)9$VB@I0g_Sza)5CZ2)wOPgaklgy?#G<_%+V;soZXAC&T$0M{Z^>^gErHaRUyS5E z2#rkm<+=PNbv=*EfiOk=h5&lRimceFw9>$%&5#Lltt;TvfX2rIMGAda>@9B;f> z2`Mq!%k}>_86xTLzxjh}f#Dq&&rC>6N=`{#%zNn>na8q@XP?M9d1$?-j*f+?qoa)b z*|X{nW$N1JFKAOPRY~S1xaeQ0zuIuEv0k0UTynj&?JA`yUbOwzb?&ZsNoi?r^ZNtl z8bd=G!y0l=M&$JIOI(caB;Zl6reD8#`$}jsdG*Z4kMG_mihla`n($TdC7qHUn;fr1 z*I;H8j~-7_RB|_wtV+kA?Q!Y#MOJ(~Y4#TO>4nS^oG3;mt<0m#)>hTc>`0_t?ZhoD zZLaM_rX|Wec@#^To0?siJ4dKiT5f)E8j@#>FH<(ziv+q9ukgua>o3<_n<&F*LIMbEk57YuwH-16oUhAPQ}hT8sqZx1+5`n9}x!E z?L9NKRkv$1xC|$5d1A(T;euGu0 zhhJ_HY)M_KDQ0FPS5=JLPON@f6cc%+$kbypJ_HOzi+`;^8|xw^51KOLKQht$*~(KD_;v0PiMVsdGqQbSp>OW|%b zF{|S)FHxPoucb6UjJKVqn3PVz=$JPTmzR}ROjR7kVZO)Fm_HiAbcK1_K!0PHTv}op zh!i}bvZX&idr@Ua_*r2pU|(seb-XSZ#25~C6cf(6SeX?T+*VTwh5)Ju z{SGJc&wrh3dpOMkhq=)Rm2en7t+!{q&cAJ7ye&cwtOgv3)eR~x0kfKZHxC|yw8ba$ zU`pU2S*gs*8~xxvhy(1Cn=e2NWcUU%+?kdi7oN2*m{pn&9h1`&8{q>lHdb~Fmt<#w zpMZmby?`q&nh$*DTxE6^cv{+#tjdNS*4=3^D=;_cq2O}hZ*#@k<2C-{4dGc4!6lhl zWFUT*|$d0dHYt2xW52r|>K7Ic3^_Ncqb19^cUx^LDpgLxRpw^_INc54HHvg!&C{CCGYvbjNZ6 z6BA!wHF!&JvGWllEXke?po-fP1K3t`ck6r`qMz_auJOIL=1Qd+Q?2~v`GReH6*ZoE zBf2WMS5IyiUUa+P%HviRWM2AwJU$y6!S^kFu7O`>!?G+t{gmlPSA7Cd2J>D|N3N_U zJi79A@Mczfo#N&MQztYvdbQ7{ym$E`^5$qC;g$n+n;kHyAu5-K_J?~xA2b6J;`k!i zgaH2w5u1f29X))x)TK5Qx(FwVHzjFo?ivg@`n;czs>_>`6|ruQO%_&nLW;IE{)1$q zxM@dqVY+xOSt88q!;KAXJf=o3aZa`aQuhSbx<^?$#TPxtr_mz5jLPX9xAAk3bUnER zrF%2tB$wR0E>%w7k))1y&~3&FVaKmFVWRz+P2BPKXNl&_78gevx4-}w6-VY}2*(?S z5?3A3=S7L{L+d#f<%`I~@xElrV5F8rKWkkbL5sazVM)U}SjtmeO9`>F3xgBH%F6QY$?)LzUknMN%^i3l@1&HyrWmbwKQ|laPIX zF%;IoG#Cmqh6#o;42IG%Tp6SS9s!zXZuJH$nLfe~#sOjj{Ug?Z_)8-g1k7>rkE8+G z!C8RrkQhLOAayViP!|X<+Rqh?hGQb2Oh_jnEeJ7lALoX9d%#eTtS~m-*1-bhgD-&Q zLFbjFmEa@bQ)w~LU~u42;A@DvERLVRIKUJ@&md-SA5bsiHL3g%Ccr5mNr2@++yRk; zr!e>AE=tsf|{Kt(nu)@ZNb7^r*$V^}v;KJZm;3ir7!XX|p z{k*|S{+eCDQ1<>{6tnBEy?MA!FRPXJ-sz40XxzQIV8WyGf%$_@hS6Q^7ybzHBqrO?iW|N6vj+Cdc~UT{wkt!lB}_$ zg@gvZBTYorDTF)H@~l!Z*Us)nU(2L*#FGJGnIxw~9KK-XLG~_BZp+?$YS32GZq`dD zej3kpy4Nv5m%?>N>(aDr(QanJH5mCk7xdsovs?7SXNEi6OBc4@arU*Zef{WsKwa3{ zRs+6`jVm9oT6sRH_vjh1$)4-`vtG!x#7Vq&rMv2mG^X?2@lEwz?t&J}@0HFy`L@sW z(0*s`Q=;MPj81YrSo(g1I&j~#woQT7X~b6aIHmUF_M_eIg8>>8)m7ZBqfHlfz39K? zoxZ#D?k#s?X6&9B{e~;M5BAQ*M$&3`_dbY-ah%fcI#{)OwVV2@Gw&YNUj0|Z_0Od7 z14+Z!65+u-7mRL`=)qB8W7+jGQq`7tPbV(V0Ea%(sNjn9SUJaA)Cjs4X1IAYWnEia zf8fc451uIXj!9hDy@0K<;mU18j5Mu^y}mBGZINR~4Po1gI=f`PNMA1x|^uV#> zmPR*6<@Ht)PHw=k*c7Z~GP#2wTUp(LN>?Jil*R@6qkM(6*+k3yi0xG#-zFMn=4E-^_LBU^pD{CX@uP(eub4id?@7hINNv$U*Ep^j&oUXplAK$T>rt&IS z=wJP#!8JPbqBvZ8WT=3QUZk+4jgD-o2=Xtrn@;v6j~0L1=tN@ zD=qocnYeHWFJL__S&00;BISS?5e^{*oCOLEVnB;tGgo1r7UbR5-3SH&Q6=2j9Hg6d z1o0Q}9hhfF`+0+0ITkaVCUe2`=)aU3pDup_F*naO}vs#+8x`Y(AEae5tXMQFDqL%GU~Lw=I== zgc{-{tvyAuJXg^;)2!;ABwb&zc6WRSTV&_qCzYWA-JS_{{Zo%~J1&r}8txC)ta=~P zY-o`jICOsSCy(RS$K~`Kh*%y$zbE;-c8wCYTnvt9YflMtJ&#@9Rwl7~TX)uqC64`H zdM;n+7ExR$+EV4!!#~LENZ$3%S}>Kwxwamz?nN`RDNelP->_Cs7hiG+u0B zb=HgRX;Qf_;sq~R_{Iw;&t)fMSUjb_|JJEQasAZt()yV^r&r}7;Qq@ zxpHPGNb?Cx+gtCXN)r zL#3y#tUbzex796Ep`hMlgRK`4r{mvLFA}mW;2PCbSJG1CXUV7ADBp>b{ukmvX$C8X z;0VNlp<47-|F(;jK%A`XlKzJXUzlOxNacMq4U_@0Kqdy0hakwRfWcK8fB+B!SOM;E zFabb;<_a{)3iIax1uz8Q0M-DKpjbzT1W&)22I4@Fg$W!`19;KXT?w!NgdwA^BTT>w zgkug?Ae#ce01Dxoy&>!Z>3};Bkr8AJj3XnrR3@Zhr1S%N3LvKXzWM62Lwi8N2wkZ9CciI}3kagDj@HoJ94Q!R$F z?mpjUCA}1dB01GQ$DR=pZg=7xu_5HGEHt7q@#!LFB0-L-zRPdAT;MUBENsnQDqVrP zSRfWFLC(Ik<3ZxLqAVJLxU;0IY@=iA2K8X!C3fuOiu+B@lOhtyfjc(Kdl1RF&f6-z zHIvjV^)pYb!`v^LO5V}U%~G5wtYj^PIVEDIlKz0=GGZ7P@wqQH zC%I}AHApbJ9m-Y`k@1&`XTNkzCPcPP$j5&@OiJ9f+CbGx7PnHj)o!VvzfbaXvu$VG zSyi5AJkgp7V|JFrEX!0El1LXn-?&w?0+zqZ2Bli%+nl6(#8xS@id=eT%PNVeRy)HF zRSC|1m7Uf((eHQ&M7`+QQ!x%&QAxW{4m>SH0&uqB^pPh~ zA^zd#?PR>KqbPb%z+l^&o!l1p#lxeMY1Pr$h4_sL^bE#6doxBS74N9$Og*|;T+^+k zp%Zfjt2?t){;)+cb%(?NuPo`+s_=@n*xE0lV74I&T0?mp}Y>VvYKV_*iikA z{Pk1VT$z4VOox-!Op!#J%=ykMf`cEL9I1Rt=#4(%Jumu#Ro8k5Nx9&;HhM)jsf(E% z`bZJcJ4QT9`pJKVMGM`5U6T-)K8MN?)5o%@8@+UA+OJ2=2;9W(Cdxe*+H>Z>+pDRT zewlYd_Y|th+b4UQs-(G&*6;c$T4PJoX`k&bSt$71et|D1HLSV!%~53<|Ak>Fv}!cQ zQKI;Cf9_kJn|iK+ZHfkX^gFxkMye}H2QPGI$(?q&O=XVXsChfye)}|%v}~0P8vPwh zC;kRYX)(A;n93}|sgi>$WRP2s z1w$7Ys{omhT2faD3Fz-$y)gK>!2~P;laMKNO&c=vD5wN1A-8%EfVsll1%}3e zF&MW1kboH+CIMoBMlcx$wm~}rECB+6h0tvPOTZ@>3qf%LHVn69A${prL}kTV<3&J@s*TvD49N-Yh z1Rw-P!ZHhp1)M|1*AK@5QNSkzcrfBF$Ol9rRD)I8jWmSKr6oW$;2GfRA6ETG^!%rv z1S~E4k65yuJ%nE(PTD1>8S0dEOwl%MUTL(1*F?)W!c^J{n~@_;K2>-Vx2=QM;7ntv z%3XCU+koOSb`=`MSRtLZ&Ri@j!3E`#YQLuo&v(0YL*B@&FgN?e1wQ#|Pnq@#7mWN& zS9?4Y>&4vk^X7m)l{em38E<65X)dj9sL&S?Z1RTGP0g5q&%2`V3^xhG1#4ZZg-&K| zdY@PXA0^r6stSRBhO`0gZ>$yQ9+B!ft1Mcv);#n4o>v8Z8(!si9P|3XFuL&8g08&( zQqj+5T~dk^0+)j3^#h5hH?H-=^*c%z&c}@4*B4*7_2KRjl>C)Q{c8t;PZhH!Msd5o zB}!h}7wc_p?IhyL%i3OK85L&Lc{*ra)(aU6pU@_>vK&LLk1N4*!WDCHbhat3SmQc(41r> z7>bTtWo^2Fj&o`m7EI;$KkF<*3_O{3@Ni$ru7N$1gD0QvsmI8XH^q(GN$}5=9@x;F zuBJdrd!U%L%Py8iO37)DO<6fMdO~EKXJ-5w8Ga@9p$8h94E3cWU3?DC|YIz=fSzo0e4_AQvtAa|4sN21T3D{%X4r4&iB-{`oBAJ@Ll zNo-9zW_ukvPSkI*nkH^OIc0TDtN5kzAs^k)q4LU)gcP|j|Gjof&OPlc zW^#0&e2Ofe3|obZmX6B|X*ig1mXd%ufUYV!czeYOp+nJYT+WLeXtq@ndlh(*66^od z#3{k)<8SS{$J(eZvU_clTi4jJ<6Sksic=ewJrmyDvwhlX+P%r>)$JGq zl8p;)@2d=!hDp{=wM-i*;G<>B>B_QN_&Z#RJ3M{2Q98o2glPwk6J*-dxcyH=XZYRI z_c_eCt^R=dQPs_O>t1qF#(}$0xZQlX=|*AOu;UUDq0(+oPoTnPZbcrwEjorhP{a`0 zV3Xc967V8VbsTp9oy;9|kA{80C9J23(u|g~k|>aA_8A2x=!=&fqz{k?Jj1*uvAkY$uUx$(o(tJ`8Ppy6@M9bmUJcx<~`MKtlo) zfzMX93 zJ>c?4PnsGBO2P1~q?CoM6%wP7LEYlM0GZxjbU^N>E)oui3)BJR0C)foKxJSL0-KAK zKoP(XKnO?!-~{AAUj&wL2n6(nng|d@m=qBVGJ+<)y)08k|VR}|^+JDsx-NRiT$v6wBqbL?m& ze}LT8^tA%U+g4SG%`&-fp1_@(2`s9a5EWFkY*I1B5+(8|wI=CRE@RdLNkOSiSLaF- zgf&@W36o>~_Ge92ht+3d7#mQ5rTSS-%6#lxxj^p+W8xhfP@Zi{{OEw&p4p1q^Xb2Q zJpV{8OrX^Cd}iyt8zJ8ch8&;Uj>K(^m6>={w~S%T%g=f+Y(2gub`SgF<#`Eftf-X9 zdH468g-k=wNM_;mm8G$mwGT;S=Dxe%8cBOewmCm|#baurHt&8r`aNb87ZrVWw5^Hx z`1T|1)08q}+$2_D;cRuL*@~S{CCeC6sFK@zXR!O1eOi04MM^}EFuCom!UARDJ(^0D z{6$=%`m)y-NEP4~reD;a+_aU@VNv-FhIY4@T`jC9;+ zNa_f8k>u60w$CZv%%Mb71d*F8?i}OE7B&t}Ovb)?`62;jTT!M)^)4G#Q*tCq2gtfS z@nn;zQ|@gUZ}946)Zm7j`7#Ad&Dg>dmy2l95jwp7XY;6qWra5MPR+tY68j27e*RWR zDYbX@dMU6_T319kFEJIRu-iEK=7UQ6nw(7)_QQ^Z-FLo}z1oS<&?~L!6}aYIvd|t> zezqs9qa>*P>x=Ud#}`UF>b_~3Xftl=oX-_GGWkHZfj8Auv9>4sQHSuwkgY|mcRCw) zNK3o!e|uq8%V}wXcW7cWKX1h??;7mxjlNR6q>=kpGcOyYTl0^<^5r=g&fb7~Sdd%Y zgqfVR+E8+s&TYl8y)SdazRkCVw*;Lo#lPWmp@>jR>=FjQf3j1ppBxa|yX82U!Xr!4 zV{K5*ryFdxvD|>|wmeFY?en^zA|}iJ=CqvOdAEQoKCph|G&cG8fqR~j0P)(-hTBJ70Wr!Z}XPsmewOe{{lI5mhyWnWLi}!w5PifanjT@ zfDLd3KmzB$`61i%J#0b2(+AzaK%ZYfG#B$VaAATwSh5EhJYx*p?^xue+?7= z`>z6!sQwisi=#HHR39R_p{ts#WawTQTG+}wl0;g=8Yw3}7s6w0rSw|DsNGB~SIeCx_3pX&~9)Uw{4c$?a?|0TEekRKnT_2?&y>`KXZ zqZNmQBDU*4>pZG#^CrJOBZ(`EgyYtwH$CUaEaNWmH~MUKM@(^5$g7Uq*Nj6d`fysAUaJuU6G8pt>^&Nm1N~!_%5k+j!r7 zdVGELC-PRtw_iTLKX#LX**ibg{$++5gA@6T4oQquPghvgvY}K{;^!1}zJj=yMhdz9C54cJogU?i(-NG+gveOfCJHuv+pr}$G`&z+ zqW(gL-(d`%yE7R_z<6U&hmxjtimBVU!jTEbItbK(&b369jaNUity}=bH_jqZAY4z-t ztv|H()`j!+o`+tCb`S7A#&+^<+=RU~!KW4Se5*}S+|qF}RW?v#SBpGetdJ`1RyA6s z7}JQ--YY?f_3-gUrSFpRr4i$WEi`g${Uv?0^Y+GPRlXy(#yU02>U#)Vc**yh5Ad2C z8M zG?+2bF!bU~bf9B^V}oTOml_U=0k?fD}lnsPP86!FCB89$*Ar2)VRyqYGL<1St@{$+nLaX8d5aG58r*P}%r!`BX3MAtj|3rStXUqKB4AfEobUw*{u zGoi}2)rm{i9F6MO3_viR;CrykuN|*Q$sj*VIxcL%BvDBCBV3AljU$d*nl0j_G~XNlUh^RiU7CR!|t}@kO7)rw+_z$L_~Tc+j!ew~xs0 z2xPJoVu^Ko<5A5~>goHi)`4{C1qsm;9jiM~g|>m5DQ zF^#ulpYQi7_H9kuEGU+|CocIxJJ0n8?10?b2SaKCyVlvMQw=|MY~0D1y!s-^i1#Fw za6{E}GHIkJmb#z&W2wfepJZ$-dBtbw!w0ulY}*{S-OF=x#DfQf>5t^kAE3Z-g7xS=}}pfPUNYIyHO{!qV$UUrfaG8Q>V&0+NDZF z2Di#SPU-Qyx{1X$+(>2&rtn>;9sJs%wTFL9Vrl)`uLIMUTE)GV)^{4pF3oNAMN_4G z({wG+KB0B7vLWe3x_tY%NvP0Q&gTYw#oh*Bh)*&cXFEhAw{>)g50W61?K@z6ooT9~>A4R^n@Co+> zi~sd0%fH?U<80_lDp-)YfeJtZpaS?HBxxuN$ZmMCAv6vkrUupzI}xA)Or!t+KnN%U zNMT4tHW$DQutfj{U;zA@lZ+Af?`FZU2VelRrL2Mya6@1Q*fG*2e%we!cI{$72w2sF zmBTg!+#GT?QV(GBJ%%i^!IwGp09XY`0H+4m27~|=kXa5a?_nbK6?wb`r~t+dW{q4I zfQ7<4WMl=L01kmi!yPY}X#oZ}Ylt7q4rHbBa2SSgoMHfgfv0AWN|2ic*n|m?)EOTe2XrBDDuAm7uxbfMrb-XT0UJndh>HdoAnT09+o`}jU;uCq zxe2ikzLS4*NA>^0?*93u|D`_xpu%78g#EUm*O@P}XqOWgluMI#q2ZH{i9R$STGIIQ zglL;@q!q_w@K_lEFX@)rb9YWt;y=p5;b89r; zGXd@T!os#xv5liyy90cT(e}LKfizVbT#FWupytnO|rt}n@vw+zhv~u?8K;D z;gaBfcdi|KS)M$?xcsHaiuu*aQ8r$(*fCF5Zmbi#gU@6yS+Tmj8JlE3P2DIjDMBlh z5z>eqVX{;mw@`WdKQaVFI`m|DSIejr%Pb4dIW4h*O-M|&<84by-XEBJT0)^hFW@*q zJ|kZyW~eYhQNxS7lp3wgc!UkyXZJi8yUA|&M7W5*dWcARhAi*yxSO3~SENmvxUa<8 zzSw!?%dQUTWvCa*r>N`p<|+L2L`p8IjjHE!*oHN9swml6eOu<1T(1dfv5<3##e)xc zn)COz9@r;$=(4BH`SQzTP5RmFux+ODW?+y_>cfPLw%UACMy*f$TBX$N?;|WsQzHhIuL1io>&gYI?;t|3< z93o_jr%5V2*yTIZ!tcMWsfA#ipDV^dy=x}9`A=Jj$LtxF^h>|0+eSE8MedAgZ}6t<0lEE)TxS{vgNGRrI6C+=*c%deTl(SI17vv%H>MV&J~AwV zTS;{+7#tzP6WDG{anRT>=Rz0QFtUVLjPc;gV6%9{a;LQ;y_7o_cOkdyZ=bxKf zn_8Ni^vR7@y4~!)yKLRtG&y~Ndyo3$1}uk1`<_~iP3VuW9umO7lY}#G9xA`&dLV;? z2Z!cyf=yg^XHgNwr>O@-V#v0}zVbKX=jGM+~nErNnUl#VlM3&O;vedfb2;@g)ISEV>as-3axZr%MuLzh>pp#&12spv`t4fY=2rTkPNAac~t zHM=>9Ai+-0l;>Xih;`J?%Jv>LHf~F&zMsmDvYuT&7?BdExvP@7C;>s@n)7v;db&N2 zbU(2)-gz4zx~SLsre|Zv^iGHN*Mxk*jdf2t&or0X^nQHR(R6p^>4L@v)>v=(ExKTS zL&#(!Hd9tRTGsz3evvYGbHw#uaQt_v9+!DBrcQHikMI0kaS|`IlvQYg%`Dt zj@FPRj?v{-c<49Biw%%`6NKXYFakm{=2fkN%a|Ht$tu~OeUpTn&cd4`>L&ffsi^PM zP}*;Y9eI3r@oBx^4m*-*em(C$4m*-#7SB7JQ_hh`ZgVdl`QM*+2pm6-JRHyeeCk0o zza9C-`-6Wv^00^EoO;j)=itMsMNW-MeuM9?cYCK z1CQ@wb(M5=@qEQ+O3t3epDw`WCWv#HR#n&3vMyI~RWeu9HD0g18h`Od>*e~UII?lK zbkFVHJM7!t#>+YrR-hg{d^G&{;Xv}xlV{IHl2(w$rygogBw;l)OlIG|oBQ;VQCz&LAdlvQS3xN{59fA3CVK zAW|_?m6(3U0@qDH@4@zHwo?A&vGI#cheEt*;KcCN`#lOe)-F2om3l2QC7v4v<=B>9 zjAH?O#hc=npyhdY*A7&Kwi5YtNNPF)V#RA1cTTwLkgILt2{>}E<1p*_K#`b=*SA}; z#VA3c%>9>~^n!O!YX9`P-QK-8UUBs_cJA};Z)=`5t>o&9Rx~uqAj&0A$XRJ#m>b@G z>7}Hoo!UwhC;OQIsZ54Ih<{O{^jeR?L`>?qs9jssq))h}9-V;}Z8B+2GZ?T{PFtp* zo{4)^sf@N8n=2Dcx43wZjVCv*n!)fr(DhVVp{*j_f*%Q|^c=7LOka!lh&b7rz1fea zSHOfuj1^kdRydIxT=!rrS77eRxRCIH?JrJ6t`gz?E@+283wn`Wsr-O0PD$x0@aAZ` zKi2^;22=wy!`0h~DEMl`)J57Q{P9Sbl6lEKgllno>U%|Yn^WrKD> z!AMWJ(F0(EaL%C`Or`*AKsA6F2p8cQN8G7mz%xYRr6nAy!9)p60$c>}25bYZLucU# z{f{b71KoqfLFgcN4&?q{((oU?7Z9|=-wGOUAZB2IGg=fh?sNe@H$jS<`d!f6l}w92 z2wHtvx7>F@FEi{+u=tapEmT3!x)VwGrAsNl30jl3YE|+NL4UPvzC;vU6m;?#GFf<) z3JBVX`9siliYg};1#PXU?X1=%H5Q8qdKEWXmujzyI;^`WXo3(*$2J9hhRJPnM&Dz0 zJuWfJK>}s2^kI_wtj9@u&Pz+14aH^)s`WDN_(L2)Yp6^YilJ6g4(FnaM6NL%QM4N7 zB%{dGf8G+ELrhFsl|HVKi@mv8E*NgYtccW)@5T%U-AK$x9_PAhn7}6Qyej`@+h%iW3d(1KCZgG=;-? z_S!M?s+9ALr2RV!(EB13#ao>vUv3)psr287$wFah2a*-Ol_=82uW$3QPhZQV5z5w< zI7KU77(ca_`?b20gUZRd(^06i{-a59g4|B_OZwC^tfgXb517)2>G!x5q@*_5-_jg! z#>)FsJW)cC#4{EH2XL`yLJqwJjaBg~AJ4Q?psQpMOZ=P7UU^V5O2V}o8U^Vj? z_M@XPstU{c&e%$tpmIn7gfwB2%Bco=`)UbkXDvGvk>`HN%A6p# zOkcI5az2MAJ|~f0d5AA)V|r~@#22Pk$FD=pllDvzm^L#a`sh$wku$i|vBKFqOuB`% z!aJ$BXBtPy{|c!;pj{R`+RdlK7kulBjI~mhFq(Qu`81JRqZbb|MyFg0#wAbX#(tI~czf>$5dxE&fJVt9a^4$rP)#N%) zx%u}3cdxuJGZ|>Go#Dm)WcWygjuX=u;mWX~e&EW8diVT9roX5_b!?F6cG^>$C9zxz z77KP_z;T{r1&U^XhLVtcPhoSEqE^;jd*MCACZ`o8#0+cM7?qQ26ay)4DEVk^F}vFV zG*>S^)Hz~=;!0n7Qzn);GJed_!n7%qV4j|de^t2!2=8Amcw+O#dqghPe#dzJ2ZhoT zM*3Nv7_Jc}r6<2y%6Baxe1fO#)V{ks>-bi?I>riH578%!lGQ3SimYAPEurbwS}#ko zegnMipEduzo`U9)fn!TQ)KdUFpdFyzQw*^gxh4lgNsi`0?ZA7E+JWMrd_Xt=9tiy_ z-MxROb7Ted`>F~!hg$})&f*9j#1CqRnIp&^uCXlw9=U0L}sMiwOgv zJ18942rc$kkUFQrg2+Mq9L@hv+3JAiZU0vDSOaAP1HAF?61x=lubKzITm1p>N;=)L z-!-qZyfeY-Pk^^lrOr!adBS-Tdpm`w^DbRf1Z;9PbP9=k* zc{w#_g-E-5XKx?oa#lbO#ar^9rX1uIc!;IPWl8DkbF&M}@10wZ4K#=)t7hU*_;E(} zW$KY)lOta3y2oOVTy)%nGIzYMl_|EMhO(zT%EC5_g>W4=>0q|t*s<+l7T9lRNW-ALz+)X)-iFMK=kMKKmX-n52?T{xr5peUL>}hZ{erc4YRGx zpOz0=$Hj(Hctp7GUVJUvnO3H8 zGJ3Im*W7|$p3T|nWe0U?@s*$!e^EXN7&y;SJ}B+?gDK8b3bY6M1h9kV!399=AVN?( z$PCmD>V>=yfCa*XX#cLF=jS3*FHr2F$dNlyfO_x+j@m)${n@Y=t_0Tr!E?q_FqlF{ zQt*PV#TRZt$^hAe^+0CNhdsvP-63#||0z2EYuW(iTl~H9brqqj3;a&`VycGUX|Gzz zH2s6}71X2KwoEiYi>65XV^*GElqSNI1P5B0iZ{FQ=U` zgL>2v*CdlIhi*+|Wb66dt4GJD-Z2myC+@H&|XMx^f7{`%3C(eV?A?8D0|0~^r>!5c~E=Yb+<_Ow3i2<%;{0ZcgkH{_&Dej9n2Z6)Dj+5m%k_Wm2@Bb(29hCkbrgF>4%a`N%f2VW_4U_Lm*QjK! z`h(I*OMWnZiGF9osy``xl>#Vz8BFEW)GTK$g?>}IS@6(ERg*O(9xXIwtHJKzK9~?> zFwU-m$6*agBE=r*ZS6-TOatX`HRGKbaWXnH9_(?eESg44a8z}GO%7Ku%iptaq7$nd z>pC}iR|%6N;eWT^(`95GpKc1Nsy@8DuZ*TW9`1P@>sqNA`H`_})Phm7ZJB|J6XsQ+ z@Pa@TC1=gvH-aMgqYd&Z#iI!gyO(Ke3lC#ut z9c#kKXSuX-mqjh#c9PF-wb6lk-)HwLm(Ce>#aXq=R2ko?bMni zk;!xaq@e4iO)riL?%o48z{PJSiTFuuN0BzE2hKR{lhM4N9><`DwlHv+sC!obax)oB zSXM+E!NCQC<^yY*RgMXWYHU^7lxBK%G&Uy6mXaW8K)4qw(`YhyID9*iy<5gKy(vS0 z=v_FT>7f6hDTBuZdt`7ljTg z2i=3RLHYo8P&p_bR1UBOwu22IREN!dHw(-Fw2nL*`Ij)ykvnJ|Bo0Ccwu8if1o%Jc z8zKPk93+m60>2{DH$Xa=1oU=}%KwK-{P%bVxl{fg>^fu}cxLu@au-!!{$1|sl}wXA z%3brv5MFa>XM)L}gFUno_hujVS zEO*Kex%>Sp_x<1HF3OR6FOP2Qcezv4K<;jfayN5(u$+)0;=y%T!)rG1A$371pO=9m zqaLHg1h+=ZIux(tmcnddgo4U%fXZ3Iq?scvfx@C%%^596C1o1JiV2gdI( zW?#(ipmOjAxQzog^ZyT(LGZ`RnXbY+ zKb84BWz1N;*Ld%qzy9Zi;Wsjxs_6pD=WI@guPZ_wvl7dqlMXDOaqo+B;0bMx|F((L z#MHRy8cz@sq!$vnG;>Z<<`Zmv_8X!S?(WbuA}J+mM$+=F`Q!?pabf2spB-KOuJ9R} zuW7Ey?3MXCMy82vMXmp_R}_CVw@!T(2DM=`Pk_;q$`QA*0 z6ZomfiSic(dlm(b=S=2pf9>a{zhQjw;z$MUM?YsRUL1j$98`3WIRG44r!NL~5H)8S z2VzI&aEmK8n4E(o5e*_q9ica95D5ii!{8M^bj^`7SOh1UgO)-2oH0A6hl7MUV>WOA z&U6hK-IZ!1Bp(=u(hiFLx1k&^j^zLQizA`uheHai&yduP-kRLq`u>9;%PM8m>e8dP zpUoe`3k2sFv3n0#hQ%dCMKdkrm5+!mzJfaAh_d=2D?QYyg42aX1-Z8Q#plkI*yzcW zT`W+mu)(UR=&1M!)Lp)E?Z%azHCBokZEbEn5mI{xydttkvBgU3ww8#9)a^&Z1NUeT zJ4hmmkM2C~ka}wQ=ov}-#UuX5uPko!58j{rMf-#4sJ5;xdvHgAUOYeTvt?6DVNKjT z@pEh68`RHsBj7?*PvEWRDDlf)af;n->3vou1S`96MiSF-LhCE`BA3ZqfRXfOIwnuAIB$TPO{`R{|L&Wl#*K0h0^GQ z8MbP$@>-Ak_4Ogc~d~v3QdTIQBypATPij1X<7oB989wJ7;7_wWth`1t-ORpFYET3*R9BE)Wd07o#(i zqlq5m1$Ca&W^Dk!-?>xG(Q)xaS zQgVI4ssmi}RFp<*n{_KuF4=u{Ao{6N@KDtyt9wWF*VCIq{T{pK?fBs75$vnzlDU7G zwMSJ>n2)q+u-6BptF|V6`%Ab!jaEN)x=f)M%)w1-VHzc1ZIf8^yEhe&DmiCKI1&zdl3sClr8ZBj&xB^`I&CiZrB!J|w1q9!J!go*Lg&cl8ZFO>M6N9t`F)SsE+i+*dG>|Hqq|U(32StMmrB;YyG_=uv>d%FRo4e zWSEGOIjwl_e@$Qkue>#nO$HA}4GGX1LbC`V;2Oex;5CH6VxC)CC!v{F8@h&YAuo52!Zk?B d4~3WWqp5u~wU4HD;DFs|**;pf51X>x8UPz$*`5FZ diff --git a/stlearn/app/static/js/core/bootstrap-material-design.min.js b/stlearn/app/static/js/core/bootstrap-material-design.min.js deleted file mode 100644 index 0a8a9bd3..00000000 --- a/stlearn/app/static/js/core/bootstrap-material-design.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["jquery","popper.js"],e):e(t.jQuery,t.Popper)}(this,function(t,e){"use strict";function n(t,e){for(var n=0;n0?i:null}catch(t){return null}},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(n){t(n).trigger(e.end)},supportsTransitionEnd:function(){return Boolean(e)},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var r in n)if(Object.prototype.hasOwnProperty.call(n,r)){var o=n[r],s=e[r],a=s&&i.isElement(s)?"element":(l=s,{}.toString.call(l).match(/\s([a-zA-Z]+)/)[1].toLowerCase());if(!new RegExp(o).test(a))throw new Error(t.toUpperCase()+': Option "'+r+'" provided type "'+a+'" but expected type "'+o+'".')}var l}};return e=("undefined"==typeof window||!window.QUnit)&&{end:"transitionend"},t.fn.emulateTransitionEnd=n,i.supportsTransitionEnd()&&(t.event.special[i.TRANSITION_END]={bindType:e.end,delegateType:e.end,handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}}),i}(t),ur=(a="alert",c="."+(l="bs.alert"),h=(s=t).fn[a],u={CLOSE:"close"+c,CLOSED:"closed"+c,CLICK_DATA_API:"click"+c+".data-api"},d="alert",f="fade",p="show",m=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){t=t||this._element;var e=this._getRootElement(t);this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){s.removeData(this._element,l),this._element=null},e._getRootElement=function(t){var e=hr.getSelectorFromElement(t),n=!1;return e&&(n=s(e)[0]),n||(n=s(t).closest("."+d)[0]),n},e._triggerCloseEvent=function(t){var e=s.Event(u.CLOSE);return s(t).trigger(e),e},e._removeElement=function(t){var e=this;s(t).removeClass(p),hr.supportsTransitionEnd()&&s(t).hasClass(f)?s(t).one(hr.TRANSITION_END,function(n){return e._destroyElement(t,n)}).emulateTransitionEnd(150):this._destroyElement(t)},e._destroyElement=function(t){s(t).detach().trigger(u.CLOSED).remove()},t._jQueryInterface=function(e){return this.each(function(){var n=s(this),i=n.data(l);i||(i=new t(this),n.data(l,i)),"close"===e&&i[e](this)})},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},i(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),s(document).on(u.CLICK_DATA_API,'[data-dismiss="alert"]',m._handleDismiss(new m)),s.fn[a]=m._jQueryInterface,s.fn[a].Constructor=m,s.fn[a].noConflict=function(){return s.fn[a]=h,m._jQueryInterface},_="button",y="."+(v="bs.button"),E=".data-api",b=(g=t).fn[_],C="active",I="btn",T="focus",A='[data-toggle^="button"]',S='[data-toggle="buttons"]',w="input",D=".active",N=".btn",O={CLICK_DATA_API:"click"+y+E,FOCUS_BLUR_DATA_API:"focus"+y+E+" blur"+y+E},k=function(){function t(t){this._element=t}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=g(this._element).closest(S)[0];if(n){var i=g(this._element).find(w)[0];if(i){if("radio"===i.type)if(i.checked&&g(this._element).hasClass(C))t=!1;else{var r=g(n).find(D)[0];r&&g(r).removeClass(C)}if(t){if(i.hasAttribute("disabled")||n.hasAttribute("disabled")||i.classList.contains("disabled")||n.classList.contains("disabled"))return;i.checked=!g(this._element).hasClass(C),g(i).trigger("change")}i.focus(),e=!1}}e&&this._element.setAttribute("aria-pressed",!g(this._element).hasClass(C)),t&&g(this._element).toggleClass(C)},e.dispose=function(){g.removeData(this._element,v),this._element=null},t._jQueryInterface=function(e){return this.each(function(){var n=g(this).data(v);n||(n=new t(this),g(this).data(v,n)),"toggle"===e&&n[e]()})},i(t,null,[{key:"VERSION",get:function(){return"4.0.0"}}]),t}(),g(document).on(O.CLICK_DATA_API,A,function(t){t.preventDefault();var e=t.target;g(e).hasClass(I)||(e=g(e).closest(N)),k._jQueryInterface.call(g(e),"toggle")}).on(O.FOCUS_BLUR_DATA_API,A,function(t){var e=g(t.target).closest(N)[0];g(e).toggleClass(T,/^focus(in)?$/.test(t.type))}),g.fn[_]=k._jQueryInterface,g.fn[_].Constructor=k,g.fn[_].noConflict=function(){return g.fn[_]=b,k._jQueryInterface},j="carousel",L="."+(R="bs.carousel"),P=".data-api",x=($=t).fn[j],F={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0},M={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean"},Q="next",H="prev",U="left",G="right",W={SLIDE:"slide"+L,SLID:"slid"+L,KEYDOWN:"keydown"+L,MOUSEENTER:"mouseenter"+L,MOUSELEAVE:"mouseleave"+L,TOUCHEND:"touchend"+L,LOAD_DATA_API:"load"+L+P,CLICK_DATA_API:"click"+L+P},B="carousel",K="active",V="slide",Y="carousel-item-right",q="carousel-item-left",z="carousel-item-next",X="carousel-item-prev",Z={ACTIVE:".active",ACTIVE_ITEM:".active.carousel-item",ITEM:".carousel-item",NEXT_PREV:".carousel-item-next, .carousel-item-prev",INDICATORS:".carousel-indicators",DATA_SLIDE:"[data-slide], [data-slide-to]",DATA_RIDE:'[data-ride="carousel"]'},J=function(){function t(t,e){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this._config=this._getConfig(e),this._element=$(t)[0],this._indicatorsElement=$(this._element).find(Z.INDICATORS)[0],this._addEventListeners()}var e=t.prototype;return e.next=function(){this._isSliding||this._slide(Q)},e.nextWhenVisible=function(){!document.hidden&&$(this._element).is(":visible")&&"hidden"!==$(this._element).css("visibility")&&this.next()},e.prev=function(){this._isSliding||this._slide(H)},e.pause=function(t){t||(this._isPaused=!0),$(this._element).find(Z.NEXT_PREV)[0]&&hr.supportsTransitionEnd()&&(hr.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},e.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},e.to=function(t){var e=this;this._activeElement=$(this._element).find(Z.ACTIVE_ITEM)[0];var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)$(this._element).one(W.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=t>n?Q:H;this._slide(i,this._items[t])}},e.dispose=function(){$(this._element).off(L),$.removeData(this._element,R),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},e._getConfig=function(t){return t=r({},F,t),hr.typeCheckConfig(j,t,M),t},e._addEventListeners=function(){var t=this;this._config.keyboard&&$(this._element).on(W.KEYDOWN,function(e){return t._keydown(e)}),"hover"===this._config.pause&&($(this._element).on(W.MOUSEENTER,function(e){return t.pause(e)}).on(W.MOUSELEAVE,function(e){return t.cycle(e)}),"ontouchstart"in document.documentElement&&$(this._element).on(W.TOUCHEND,function(){t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout(function(e){return t.cycle(e)},500+t._config.interval)}))},e._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},e._getItemIndex=function(t){return this._items=$.makeArray($(t).parent().find(Z.ITEM)),this._items.indexOf(t)},e._getItemByDirection=function(t,e){var n=t===Q,i=t===H,r=this._getItemIndex(e),o=this._items.length-1;if((i&&0===r||n&&r===o)&&!this._config.wrap)return e;var s=(r+(t===H?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},e._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex($(this._element).find(Z.ACTIVE_ITEM)[0]),r=$.Event(W.SLIDE,{relatedTarget:t,direction:e,from:i,to:n});return $(this._element).trigger(r),r},e._setActiveIndicatorElement=function(t){if(this._indicatorsElement){$(this._indicatorsElement).find(Z.ACTIVE).removeClass(K);var e=this._indicatorsElement.children[this._getItemIndex(t)];e&&$(e).addClass(K)}},e._slide=function(t,e){var n,i,r,o=this,s=$(this._element).find(Z.ACTIVE_ITEM)[0],a=this._getItemIndex(s),l=e||s&&this._getItemByDirection(t,s),c=this._getItemIndex(l),h=Boolean(this._interval);if(t===Q?(n=q,i=z,r=U):(n=Y,i=X,r=G),l&&$(l).hasClass(K))this._isSliding=!1;else if(!this._triggerSlideEvent(l,r).isDefaultPrevented()&&s&&l){this._isSliding=!0,h&&this.pause(),this._setActiveIndicatorElement(l);var u=$.Event(W.SLID,{relatedTarget:l,direction:r,from:a,to:c});hr.supportsTransitionEnd()&&$(this._element).hasClass(V)?($(l).addClass(i),hr.reflow(l),$(s).addClass(n),$(l).addClass(n),$(s).one(hr.TRANSITION_END,function(){$(l).removeClass(n+" "+i).addClass(K),$(s).removeClass(K+" "+i+" "+n),o._isSliding=!1,setTimeout(function(){return $(o._element).trigger(u)},0)}).emulateTransitionEnd(600)):($(s).removeClass(K),$(l).addClass(K),this._isSliding=!1,$(this._element).trigger(u)),h&&this.cycle()}},t._jQueryInterface=function(e){return this.each(function(){var n=$(this).data(R),i=r({},F,$(this).data());"object"==typeof e&&(i=r({},i,e));var o="string"==typeof e?e:i.slide;if(n||(n=new t(this,i),$(this).data(R,n)),"number"==typeof e)n.to(e);else if("string"==typeof o){if(void 0===n[o])throw new TypeError('No method named "'+o+'"');n[o]()}else i.interval&&(n.pause(),n.cycle())})},t._dataApiClickHandler=function(e){var n=hr.getSelectorFromElement(this);if(n){var i=$(n)[0];if(i&&$(i).hasClass(B)){var o=r({},$(i).data(),$(this).data()),s=this.getAttribute("data-slide-to");s&&(o.interval=!1),t._jQueryInterface.call($(i),o),s&&$(i).data(R).to(s),e.preventDefault()}}},i(t,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return F}}]),t}(),$(document).on(W.CLICK_DATA_API,Z.DATA_SLIDE,J._dataApiClickHandler),$(window).on(W.LOAD_DATA_API,function(){$(Z.DATA_RIDE).each(function(){var t=$(this);J._jQueryInterface.call(t,t.data())})}),$.fn[j]=J._jQueryInterface,$.fn[j].Constructor=J,$.fn[j].noConflict=function(){return $.fn[j]=x,J._jQueryInterface},et="collapse",it="."+(nt="bs.collapse"),rt=(tt=t).fn[et],ot={toggle:!0,parent:""},st={toggle:"boolean",parent:"(string|element)"},at={SHOW:"show"+it,SHOWN:"shown"+it,HIDE:"hide"+it,HIDDEN:"hidden"+it,CLICK_DATA_API:"click"+it+".data-api"},lt="show",ct="collapse",ht="collapsing",ut="collapsed",dt="width",ft="height",pt={ACTIVES:".show, .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},mt=function(){function t(t,e){this._isTransitioning=!1,this._element=t,this._config=this._getConfig(e),this._triggerArray=tt.makeArray(tt('[data-toggle="collapse"][href="#'+t.id+'"],[data-toggle="collapse"][data-target="#'+t.id+'"]'));for(var n=tt(pt.DATA_TOGGLE),i=0;i0&&(this._selector=o,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var e=t.prototype;return e.toggle=function(){tt(this._element).hasClass(lt)?this.hide():this.show()},e.show=function(){var e,n,i=this;if(!this._isTransitioning&&!tt(this._element).hasClass(lt)&&(this._parent&&0===(e=tt.makeArray(tt(this._parent).find(pt.ACTIVES).filter('[data-parent="'+this._config.parent+'"]'))).length&&(e=null),!(e&&(n=tt(e).not(this._selector).data(nt))&&n._isTransitioning))){var r=tt.Event(at.SHOW);if(tt(this._element).trigger(r),!r.isDefaultPrevented()){e&&(t._jQueryInterface.call(tt(e).not(this._selector),"hide"),n||tt(e).data(nt,null));var o=this._getDimension();tt(this._element).removeClass(ct).addClass(ht),this._element.style[o]=0,this._triggerArray.length>0&&tt(this._triggerArray).removeClass(ut).attr("aria-expanded",!0),this.setTransitioning(!0);var s=function(){tt(i._element).removeClass(ht).addClass(ct).addClass(lt),i._element.style[o]="",i.setTransitioning(!1),tt(i._element).trigger(at.SHOWN)};if(hr.supportsTransitionEnd()){var a="scroll"+(o[0].toUpperCase()+o.slice(1));tt(this._element).one(hr.TRANSITION_END,s).emulateTransitionEnd(600),this._element.style[o]=this._element[a]+"px"}else s()}}},e.hide=function(){var t=this;if(!this._isTransitioning&&tt(this._element).hasClass(lt)){var e=tt.Event(at.HIDE);if(tt(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();if(this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",hr.reflow(this._element),tt(this._element).addClass(ht).removeClass(ct).removeClass(lt),this._triggerArray.length>0)for(var i=0;i0&&tt(e).toggleClass(ut,!n).attr("aria-expanded",n)}},t._getTargetFromElement=function(t){var e=hr.getSelectorFromElement(t);return e?tt(e)[0]:null},t._jQueryInterface=function(e){return this.each(function(){var n=tt(this),i=n.data(nt),o=r({},ot,n.data(),"object"==typeof e&&e);if(!i&&o.toggle&&/show|hide/.test(e)&&(o.toggle=!1),i||(i=new t(this,o),n.data(nt,i)),"string"==typeof e){if(void 0===i[e])throw new TypeError('No method named "'+e+'"');i[e]()}})},i(t,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return ot}}]),t}(),tt(document).on(at.CLICK_DATA_API,pt.DATA_TOGGLE,function(t){"A"===t.currentTarget.tagName&&t.preventDefault();var e=tt(this),n=hr.getSelectorFromElement(this);tt(n).each(function(){var t=tt(this),n=t.data(nt)?"toggle":e.data();mt._jQueryInterface.call(t,n)})}),tt.fn[et]=mt._jQueryInterface,tt.fn[et].Constructor=mt,tt.fn[et].noConflict=function(){return tt.fn[et]=rt,mt._jQueryInterface},_t="modal",yt="."+(vt="bs.modal"),Et=(gt=t).fn[_t],bt={backdrop:!0,keyboard:!0,focus:!0,show:!0},Ct={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean",show:"boolean"},It={HIDE:"hide"+yt,HIDDEN:"hidden"+yt,SHOW:"show"+yt,SHOWN:"shown"+yt,FOCUSIN:"focusin"+yt,RESIZE:"resize"+yt,CLICK_DISMISS:"click.dismiss"+yt,KEYDOWN_DISMISS:"keydown.dismiss"+yt,MOUSEUP_DISMISS:"mouseup.dismiss"+yt,MOUSEDOWN_DISMISS:"mousedown.dismiss"+yt,CLICK_DATA_API:"click"+yt+".data-api"},Tt="modal-scrollbar-measure",At="modal-backdrop",St="modal-open",wt="fade",Dt="show",Nt={DIALOG:".modal-dialog",DATA_TOGGLE:'[data-toggle="modal"]',DATA_DISMISS:'[data-dismiss="modal"]',FIXED_CONTENT:".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",STICKY_CONTENT:".sticky-top",NAVBAR_TOGGLER:".navbar-toggler"},Ot=function(){function t(t,e){this._config=this._getConfig(e),this._element=t,this._dialog=gt(t).find(Nt.DIALOG)[0],this._backdrop=null,this._isShown=!1,this._isBodyOverflowing=!1,this._ignoreBackdropClick=!1,this._originalBodyPadding=0,this._scrollbarWidth=0}var e=t.prototype;return e.toggle=function(t){return this._isShown?this.hide():this.show(t)},e.show=function(t){var e=this;if(!this._isTransitioning&&!this._isShown){hr.supportsTransitionEnd()&>(this._element).hasClass(wt)&&(this._isTransitioning=!0);var n=gt.Event(It.SHOW,{relatedTarget:t});gt(this._element).trigger(n),this._isShown||n.isDefaultPrevented()||(this._isShown=!0,this._checkScrollbar(),this._setScrollbar(),this._adjustDialog(),gt(document.body).addClass(St),this._setEscapeEvent(),this._setResizeEvent(),gt(this._element).on(It.CLICK_DISMISS,Nt.DATA_DISMISS,function(t){return e.hide(t)}),gt(this._dialog).on(It.MOUSEDOWN_DISMISS,function(){gt(e._element).one(It.MOUSEUP_DISMISS,function(t){gt(t.target).is(e._element)&&(e._ignoreBackdropClick=!0)})}),this._showBackdrop(function(){return e._showElement(t)}))}},e.hide=function(t){var e=this;if(t&&t.preventDefault(),!this._isTransitioning&&this._isShown){var n=gt.Event(It.HIDE);if(gt(this._element).trigger(n),this._isShown&&!n.isDefaultPrevented()){this._isShown=!1;var i=hr.supportsTransitionEnd()&>(this._element).hasClass(wt);i&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),gt(document).off(It.FOCUSIN),gt(this._element).removeClass(Dt),gt(this._element).off(It.CLICK_DISMISS),gt(this._dialog).off(It.MOUSEDOWN_DISMISS),i?gt(this._element).one(hr.TRANSITION_END,function(t){return e._hideModal(t)}).emulateTransitionEnd(300):this._hideModal()}}},e.dispose=function(){gt.removeData(this._element,vt),gt(window,document,this._element,this._backdrop).off(yt),this._config=null,this._element=null,this._dialog=null,this._backdrop=null,this._isShown=null,this._isBodyOverflowing=null,this._ignoreBackdropClick=null,this._scrollbarWidth=null},e.handleUpdate=function(){this._adjustDialog()},e._getConfig=function(t){return t=r({},bt,t),hr.typeCheckConfig(_t,t,Ct),t},e._showElement=function(t){var e=this,n=hr.supportsTransitionEnd()&>(this._element).hasClass(wt);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.scrollTop=0,n&&hr.reflow(this._element),gt(this._element).addClass(Dt),this._config.focus&&this._enforceFocus();var i=gt.Event(It.SHOWN,{relatedTarget:t}),r=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,gt(e._element).trigger(i)};n?gt(this._dialog).one(hr.TRANSITION_END,r).emulateTransitionEnd(300):r()},e._enforceFocus=function(){var t=this;gt(document).off(It.FOCUSIN).on(It.FOCUSIN,function(e){document!==e.target&&t._element!==e.target&&0===gt(t._element).has(e.target).length&&t._element.focus()})},e._setEscapeEvent=function(){var t=this;this._isShown&&this._config.keyboard?gt(this._element).on(It.KEYDOWN_DISMISS,function(e){27===e.which&&(e.preventDefault(),t.hide())}):this._isShown||gt(this._element).off(It.KEYDOWN_DISMISS)},e._setResizeEvent=function(){var t=this;this._isShown?gt(window).on(It.RESIZE,function(e){return t.handleUpdate(e)}):gt(window).off(It.RESIZE)},e._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._isTransitioning=!1,this._showBackdrop(function(){gt(document.body).removeClass(St),t._resetAdjustments(),t._resetScrollbar(),gt(t._element).trigger(It.HIDDEN)})},e._removeBackdrop=function(){this._backdrop&&(gt(this._backdrop).remove(),this._backdrop=null)},e._showBackdrop=function(t){var e=this,n=gt(this._element).hasClass(wt)?wt:"";if(this._isShown&&this._config.backdrop){var i=hr.supportsTransitionEnd()&&n;if(this._backdrop=document.createElement("div"),this._backdrop.className=At,n&>(this._backdrop).addClass(n),gt(this._backdrop).appendTo(document.body),gt(this._element).on(It.CLICK_DISMISS,function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._element.focus():e.hide())}),i&&hr.reflow(this._backdrop),gt(this._backdrop).addClass(Dt),!t)return;if(!i)return void t();gt(this._backdrop).one(hr.TRANSITION_END,t).emulateTransitionEnd(150)}else if(!this._isShown&&this._backdrop){gt(this._backdrop).removeClass(Dt);var r=function(){e._removeBackdrop(),t&&t()};hr.supportsTransitionEnd()&>(this._element).hasClass(wt)?gt(this._backdrop).one(hr.TRANSITION_END,r).emulateTransitionEnd(150):r()}else t&&t()},e._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},e._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},e._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},Ht="show",Ut="out",Gt={HIDE:"hide"+Rt,HIDDEN:"hidden"+Rt,SHOW:"show"+Rt,SHOWN:"shown"+Rt,INSERTED:"inserted"+Rt,CLICK:"click"+Rt,FOCUSIN:"focusin"+Rt,FOCUSOUT:"focusout"+Rt,MOUSEENTER:"mouseenter"+Rt,MOUSELEAVE:"mouseleave"+Rt},Wt="fade",Bt="show",Kt=".tooltip-inner",Vt=".arrow",Yt="hover",qt="focus",zt="click",Xt="manual",Zt=function(){function t(t,n){if(void 0===e)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(n),this.tip=null,this._setListeners()}var n=t.prototype;return n.enable=function(){this._isEnabled=!0},n.disable=function(){this._isEnabled=!1},n.toggleEnabled=function(){this._isEnabled=!this._isEnabled},n.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=kt(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),kt(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(kt(this.getTipElement()).hasClass(Bt))return void this._leave(null,this);this._enter(null,this)}},n.dispose=function(){clearTimeout(this._timeout),kt.removeData(this.element,this.constructor.DATA_KEY),kt(this.element).off(this.constructor.EVENT_KEY),kt(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&kt(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,null!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},n.show=function(){var n=this;if("none"===kt(this.element).css("display"))throw new Error("Please use show on visible elements");var i=kt.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){kt(this.element).trigger(i);var r=kt.contains(this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!r)return;var o=this.getTipElement(),s=hr.getUID(this.constructor.NAME);o.setAttribute("id",s),this.element.setAttribute("aria-describedby",s),this.setContent(),this.config.animation&&kt(o).addClass(Wt);var a="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,l=this._getAttachment(a);this.addAttachmentClass(l);var c=!1===this.config.container?document.body:kt(this.config.container);kt(o).data(this.constructor.DATA_KEY,this),kt.contains(this.element.ownerDocument.documentElement,this.tip)||kt(o).appendTo(c),kt(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new e(this.element,o,{placement:l,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:Vt},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&n._handlePopperPlacementChange(t)},onUpdate:function(t){n._handlePopperPlacementChange(t)}}),kt(o).addClass(Bt),"ontouchstart"in document.documentElement&&kt("body").children().on("mouseover",null,kt.noop);var h=function(){n.config.animation&&n._fixTransition();var t=n._hoverState;n._hoverState=null,kt(n.element).trigger(n.constructor.Event.SHOWN),t===Ut&&n._leave(null,n)};hr.supportsTransitionEnd()&&kt(this.tip).hasClass(Wt)?kt(this.tip).one(hr.TRANSITION_END,h).emulateTransitionEnd(t._TRANSITION_DURATION):h()}},n.hide=function(t){var e=this,n=this.getTipElement(),i=kt.Event(this.constructor.Event.HIDE),r=function(){e._hoverState!==Ht&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),kt(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};kt(this.element).trigger(i),i.isDefaultPrevented()||(kt(n).removeClass(Bt),"ontouchstart"in document.documentElement&&kt("body").children().off("mouseover",null,kt.noop),this._activeTrigger[zt]=!1,this._activeTrigger[qt]=!1,this._activeTrigger[Yt]=!1,hr.supportsTransitionEnd()&&kt(this.tip).hasClass(Wt)?kt(n).one(hr.TRANSITION_END,r).emulateTransitionEnd(150):r(),this._hoverState="")},n.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},n.isWithContent=function(){return Boolean(this.getTitle())},n.addAttachmentClass=function(t){kt(this.getTipElement()).addClass(Pt+"-"+t)},n.getTipElement=function(){return this.tip=this.tip||kt(this.config.template)[0],this.tip},n.setContent=function(){var t=kt(this.getTipElement());this.setElementContent(t.find(Kt),this.getTitle()),t.removeClass(Wt+" "+Bt)},n.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?kt(e).parent().is(t)||t.empty().append(e):t.text(kt(e).text()):t[n?"html":"text"](e)},n.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},n._getAttachment=function(t){return Mt[t.toUpperCase()]},n._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach(function(e){if("click"===e)kt(t.element).on(t.constructor.Event.CLICK,t.config.selector,function(e){return t.toggle(e)});else if(e!==Xt){var n=e===Yt?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,i=e===Yt?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;kt(t.element).on(n,t.config.selector,function(e){return t._enter(e)}).on(i,t.config.selector,function(e){return t._leave(e)})}kt(t.element).closest(".modal").on("hide.bs.modal",function(){return t.hide()})}),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},n._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},n._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||kt(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),kt(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?qt:Yt]=!0),kt(e.getTipElement()).hasClass(Bt)||e._hoverState===Ht?e._hoverState=Ht:(clearTimeout(e._timeout),e._hoverState=Ht,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Ht&&e.show()},e.config.delay.show):e.show())},n._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||kt(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),kt(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?qt:Yt]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Ut,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Ut&&e.hide()},e.config.delay.hide):e.hide())},n._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},n._getConfig=function(t){return"number"==typeof(t=r({},this.constructor.Default,kt(this.element).data(),t)).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),hr.typeCheckConfig($t,t,this.constructor.DefaultType),t},n._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},n._cleanTipClass=function(){var t=kt(this.getTipElement()),e=t.attr("class").match(xt);null!==e&&e.length>0&&t.removeClass(e.join(""))},n._handlePopperPlacementChange=function(t){this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},n._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(kt(t).removeClass(Wt),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},t._jQueryInterface=function(e){return this.each(function(){var n=kt(this).data(jt),i="object"==typeof e&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new t(this,i),kt(this).data(jt,n)),"string"==typeof e)){if(void 0===n[e])throw new TypeError('No method named "'+e+'"');n[e]()}})},i(t,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return Qt}},{key:"NAME",get:function(){return $t}},{key:"DATA_KEY",get:function(){return jt}},{key:"Event",get:function(){return Gt}},{key:"EVENT_KEY",get:function(){return Rt}},{key:"DefaultType",get:function(){return Ft}}]),t}(),kt.fn[$t]=Zt._jQueryInterface,kt.fn[$t].Constructor=Zt,kt.fn[$t].noConflict=function(){return kt.fn[$t]=Lt,Zt._jQueryInterface},Zt),dr=(te="popover",ne="."+(ee="bs.popover"),ie=(Jt=t).fn[te],re="bs-popover",oe=new RegExp("(^|\\s)"+re+"\\S+","g"),se=r({},ur.Default,{placement:"right",trigger:"click",content:"",template:''}),ae=r({},ur.DefaultType,{content:"(string|element|function)"}),le="fade",ce="show",he=".popover-header",ue=".popover-body",de={HIDE:"hide"+ne,HIDDEN:"hidden"+ne,SHOW:"show"+ne,SHOWN:"shown"+ne,INSERTED:"inserted"+ne,CLICK:"click"+ne,FOCUSIN:"focusin"+ne,FOCUSOUT:"focusout"+ne,MOUSEENTER:"mouseenter"+ne,MOUSELEAVE:"mouseleave"+ne},fe=function(t){function e(){return t.apply(this,arguments)||this}o(e,t);var n=e.prototype;return n.isWithContent=function(){return this.getTitle()||this._getContent()},n.addAttachmentClass=function(t){Jt(this.getTipElement()).addClass(re+"-"+t)},n.getTipElement=function(){return this.tip=this.tip||Jt(this.config.template)[0],this.tip},n.setContent=function(){var t=Jt(this.getTipElement());this.setElementContent(t.find(he),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(ue),e),t.removeClass(le+" "+ce)},n._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},n._cleanTipClass=function(){var t=Jt(this.getTipElement()),e=t.attr("class").match(oe);null!==e&&e.length>0&&t.removeClass(e.join(""))},e._jQueryInterface=function(t){return this.each(function(){var n=Jt(this).data(ee),i="object"==typeof t?t:null;if((n||!/destroy|hide/.test(t))&&(n||(n=new e(this,i),Jt(this).data(ee,n)),"string"==typeof t)){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}})},i(e,null,[{key:"VERSION",get:function(){return"4.0.0"}},{key:"Default",get:function(){return se}},{key:"NAME",get:function(){return te}},{key:"DATA_KEY",get:function(){return ee}},{key:"Event",get:function(){return de}},{key:"EVENT_KEY",get:function(){return ne}},{key:"DefaultType",get:function(){return ae}}]),e}(ur),Jt.fn[te]=fe._jQueryInterface,Jt.fn[te].Constructor=fe,Jt.fn[te].noConflict=function(){return Jt.fn[te]=ie,fe._jQueryInterface},me="scrollspy",_e="."+(ge="bs.scrollspy"),ve=(pe=t).fn[me],ye={offset:10,method:"auto",target:""},Ee={offset:"number",method:"string",target:"(string|element)"},be={ACTIVATE:"activate"+_e,SCROLL:"scroll"+_e,LOAD_DATA_API:"load"+_e+".data-api"},Ce="dropdown-item",Ie="active",Te={DATA_SPY:'[data-spy="scroll"]',ACTIVE:".active",NAV_LIST_GROUP:".nav, .list-group",NAV_LINKS:".nav-link",NAV_ITEMS:".nav-item",LIST_ITEMS:".list-group-item",DROPDOWN:".dropdown",DROPDOWN_ITEMS:".dropdown-item",DROPDOWN_TOGGLE:".dropdown-toggle"},Ae="offset",Se="position",we=function(){function t(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" "+Te.NAV_LINKS+","+this._config.target+" "+Te.LIST_ITEMS+","+this._config.target+" "+Te.DROPDOWN_ITEMS,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,pe(this._scrollElement).on(be.SCROLL,function(t){return n._process(t)}),this.refresh(),this._process()}var e=t.prototype;return e.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?Ae:Se,n="auto"===this._config.method?e:this._config.method,i=n===Se?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),pe.makeArray(pe(this._selector)).map(function(t){var e,r=hr.getSelectorFromElement(t);if(r&&(e=pe(r)[0]),e){var o=e.getBoundingClientRect();if(o.width||o.height)return[pe(e)[n]().top+i,r]}return null}).filter(function(t){return t}).sort(function(t,e){return t[0]-e[0]}).forEach(function(e){t._offsets.push(e[0]),t._targets.push(e[1])})},e.dispose=function(){pe.removeData(this._element,ge),pe(this._scrollElement).off(_e),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},e._getConfig=function(t){if("string"!=typeof(t=r({},ye,t)).target){var e=pe(t.target).attr("id");e||(e=hr.getUID(me),pe(t.target).attr("id",e)),t.target="#"+e}return hr.typeCheckConfig(me,t,Ee),t},e._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},e._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},e._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},e._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var r=this._offsets.length;r--;){this._activeTarget!==this._targets[r]&&t>=this._offsets[r]&&(void 0===this._offsets[r+1]||t0&&(!t.ctrlKey&&!t.metaKey&&!t.altKey&&8!==t.which&&9!==t.which&&13!==t.which&&16!==t.which&&17!==t.which&&20!==t.which&&27!==t.which)},assert:function(t,e,n){if(e)throw void 0===!t&&t.css("border","1px solid red"),console.error(n,t),n},describe:function(t){return void 0===t?"undefined":0===t.length?"(no matching elements)":t[0].outerHTML.split(">")[0]+">"}};return function(){t=function(){if(window.QUnit)return!1;var t=document.createElement("bmd");for(var e in n)if(void 0!==t.style[e])return n[e];return!1}();for(var i in n)e+=" "+n[i]}(),i}(jQuery)),fr=(Ke=jQuery,Ve="is-filled",Ye="is-focused",qe={BMD_FORM_GROUP:"."+"bmd-form-group"},ze={},function(){function t(t,e,n){void 0===n&&(n={}),this.$element=t,this.config=Ke.extend(!0,{},ze,e);for(var i in n)this[i]=n[i]}var e=t.prototype;return e.dispose=function(t){this.$element.data(t,null),this.$element=null,this.config=null},e.addFormGroupFocus=function(){this.$element.prop("disabled")||this.$bmdFormGroup.addClass(Ye)},e.removeFormGroupFocus=function(){this.$bmdFormGroup.removeClass(Ye)},e.removeIsFilled=function(){this.$bmdFormGroup.removeClass(Ve)},e.addIsFilled=function(){this.$bmdFormGroup.addClass(Ve)},e.findMdbFormGroup=function(t){void 0===t&&(t=!0);var e=this.$element.closest(qe.BMD_FORM_GROUP);return 0===e.length&&t&&Ke.error("Failed to find "+qe.BMD_FORM_GROUP+" for "+dr.describe(this.$element)),e},t}()),pr=(Xe=jQuery,tn="has-danger",en="input-group",nn={FORM_GROUP:"."+"form-group",BMD_FORM_GROUP:"."+(Ze="bmd-form-group"),BMD_LABEL_WILDCARD:"label[class^='"+(Je="bmd-label")+"'], label[class*=' "+Je+"']"},rn={validate:!1,formGroup:{required:!1},bmdFormGroup:{template:"",create:!0,required:!0},label:{required:!1,selectors:[".form-control-label","> label"],className:"bmd-label-static"},requiredClasses:[],invalidComponentMatches:[],convertInputSizeVariations:!0},on={"form-control-lg":"bmd-form-group-lg","form-control-sm":"bmd-form-group-sm"},function(t){function e(e,n,i){var r;return void 0===i&&(i={}),(r=t.call(this,e,Xe.extend(!0,{},rn,n),i)||this)._rejectInvalidComponentMatches(),r.rejectWithoutRequiredStructure(),r._rejectWithoutRequiredClasses(),r.$formGroup=r.findFormGroup(r.config.formGroup.required),r.$bmdFormGroup=r.resolveMdbFormGroup(),r.$bmdLabel=r.resolveMdbLabel(),r.resolveMdbFormGroupSizing(),r.addFocusListener(),r.addChangeListener(),""!=r.$element.val()&&r.addIsFilled(),r}o(e,t);var n=e.prototype;return n.dispose=function(e){t.prototype.dispose.call(this,e),this.$bmdFormGroup=null,this.$formGroup=null},n.rejectWithoutRequiredStructure=function(){},n.addFocusListener=function(){var t=this;this.$element.on("focus",function(){t.addFormGroupFocus()}).on("blur",function(){t.removeFormGroupFocus()})},n.addChangeListener=function(){var t=this;this.$element.on("keydown paste",function(e){dr.isChar(e)&&t.addIsFilled()}).on("keyup change",function(){t.isEmpty()?t.removeIsFilled():t.addIsFilled(),t.config.validate&&(void 0===t.$element[0].checkValidity||t.$element[0].checkValidity()?t.removeHasDanger():t.addHasDanger())})},n.addHasDanger=function(){this.$bmdFormGroup.addClass(tn)},n.removeHasDanger=function(){this.$bmdFormGroup.removeClass(tn)},n.isEmpty=function(){return null===this.$element.val()||void 0===this.$element.val()||""===this.$element.val()},n.resolveMdbFormGroup=function(){var t=this.findMdbFormGroup(!1);return void 0!==t&&0!==t.length||(!this.config.bmdFormGroup.create||void 0!==this.$formGroup&&0!==this.$formGroup.length?this.$formGroup.addClass(Ze):this.outerElement().parent().hasClass(en)?this.outerElement().parent().wrap(this.config.bmdFormGroup.template):this.outerElement().wrap(this.config.bmdFormGroup.template),t=this.findMdbFormGroup(this.config.bmdFormGroup.required)),t},n.outerElement=function(){return this.$element},n.resolveMdbLabel=function(){var t=this.$bmdFormGroup.find(nn.BMD_LABEL_WILDCARD);return void 0!==t&&0!==t.length||void 0===(t=this.findMdbLabel(this.config.label.required))||0===t.length||t.addClass(this.config.label.className),t},n.findMdbLabel=function(t){void 0===t&&(t=!0);var e=null,n=this.config.label.selectors,i=Array.isArray(n),r=0;for(n=i?n:n[Symbol.iterator]();;){var o;if(i){if(r>=n.length)break;o=n[r++]}else{if((r=n.next()).done)break;o=r.value}var s=o;if(void 0!==(e=Xe.isFunction(s)?s(this):this.$bmdFormGroup.find(s))&&e.length>0)break}return 0===e.length&&t&&Xe.error("Failed to find "+nn.BMD_LABEL_WILDCARD+" within form-group for "+dr.describe(this.$element)),e},n.findFormGroup=function(t){void 0===t&&(t=!0);var e=this.$element.closest(nn.FORM_GROUP);return 0===e.length&&t&&Xe.error("Failed to find "+nn.FORM_GROUP+" for "+dr.describe(this.$element)),e},n.resolveMdbFormGroupSizing=function(){if(this.config.convertInputSizeVariations)for(var t in on)this.$element.hasClass(t)&&this.$bmdFormGroup.addClass(on[t])},n._rejectInvalidComponentMatches=function(){var t=this.config.invalidComponentMatches,e=Array.isArray(t),n=0;for(t=e?t:t[Symbol.iterator]();;){var i;if(e){if(n>=t.length)break;i=t[n++]}else{if((n=t.next()).done)break;i=n.value}i.rejectMatch(this.constructor.name,this.$element)}},n._rejectWithoutRequiredClasses=function(){var t=this.config.requiredClasses,e=Array.isArray(t),n=0;for(t=e?t:t[Symbol.iterator]();;){var i;if(e){if(n>=t.length)break;i=t[n++]}else{if((n=t.next()).done)break;i=n.value}var r=i,o=!1;if(-1!==r.indexOf("||")){var s=r.split("||"),a=Array.isArray(s),l=0;for(s=a?s:s[Symbol.iterator]();;){var c;if(a){if(l>=s.length)break;c=s[l++]}else{if((l=s.next()).done)break;c=l.value}var h=c;if(this.$element.hasClass(h)){o=!0;break}}}else this.$element.hasClass(r)&&(o=!0);o||Xe.error(this.constructor.name+" element: "+dr.describe(this.$element)+" requires class: "+r)}},e}(fr)),mr=(sn=jQuery,an={label:{required:!1}},ln="label",function(t){function e(e,n,i){var r;return(r=t.call(this,e,sn.extend(!0,{},an,n),i)||this).decorateMarkup(),r}o(e,t);var n=e.prototype;return n.decorateMarkup=function(){var t=sn(this.config.template);this.$element.after(t),!1!==this.config.ripples&&t.bmdRipples()},n.outerElement=function(){return this.$element.parent().closest("."+this.outerClass)},n.rejectWithoutRequiredStructure=function(){dr.assert(this.$element,"label"===!this.$element.parent().prop("tagName"),this.constructor.name+"'s "+dr.describe(this.$element)+" parent element should be