From cd598408aee5362de9a9797ec5d392e322c766e1 Mon Sep 17 00:00:00 2001 From: sa3520 Date: Mon, 2 Mar 2026 15:02:41 -0500 Subject: [PATCH 1/8] update initialization of SpatialQuery object --- src/vitessce/widget_plugins/spatial_query.py | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/vitessce/widget_plugins/spatial_query.py b/src/vitessce/widget_plugins/spatial_query.py index 2ed0cf09..3c680bf2 100644 --- a/src/vitessce/widget_plugins/spatial_query.py +++ b/src/vitessce/widget_plugins/spatial_query.py @@ -123,7 +123,13 @@ class SpatialQueryPlugin(VitesscePlugin): plugin_esm = PLUGIN_ESM commands = {} - def __init__(self, adata, spatial_key="X_spatial", label_key="cell_type"): + def __init__(self, + adata, + spatial_key="X_spatial", + label_key="cell_type", + feature_name="gene_name", + if_lognorm=True, + ): """ Construct a new Vitessce widget. @@ -131,6 +137,8 @@ def __init__(self, adata, spatial_key="X_spatial", label_key="cell_type"): :type adata: anndata.AnnData :param str spatial_key: The key in adata.obsm that contains the (x, y) coordinates of each cell. By default, "X_spatial". :param str label_key: The column in adata.obs that contains the cell type labels. By default, "cell_type". + :param str feature_name: The key in adata.var that contains the gene names. By default, "gene_name". + :param bool if_lognorm: Whether the data in adata.X need to be log-normalized. If input is count data, set to True. By default, True. .. code-block:: python @@ -140,14 +148,22 @@ def __init__(self, adata, spatial_key="X_spatial", label_key="cell_type"): # ... vc.widget(plugins=[plugin], remount_on_uid_change=False) """ - from SpatialQuery.spatial_query import spatial_query + from SpatialQuery import spatial_query import matplotlib.pyplot as plt # Add as dependency / optional dependency? self.adata = adata self.spatial_key = spatial_key self.label_key = label_key - self.tt = spatial_query(adata=adata, dataset='test', spatial_key=spatial_key, label_key=label_key, leaf_size=10) + self.tt = spatial_query( + adata=adata, + dataset='test', + spatial_key=spatial_key, + label_key=label_key, + leaf_size=10, + build_gene_index=False, + if_lognorm=if_lognorm, + ) self.tab20_rgb = [[int(r * 255), int(g * 255), int(b * 255)] for (r, g, b, a) in [plt.cm.tab20(i) for i in range(20)]] From c0d4b17b0f6147c35048ecd6432be872dc4cee41 Mon Sep 17 00:00:00 2001 From: sa3520 Date: Mon, 2 Mar 2026 17:04:16 -0500 Subject: [PATCH 2/8] set various colors for motifs, change default parameters --- src/vitessce/widget_plugins/spatial_query.py | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/vitessce/widget_plugins/spatial_query.py b/src/vitessce/widget_plugins/spatial_query.py index 3c680bf2..56117c83 100644 --- a/src/vitessce/widget_plugins/spatial_query.py +++ b/src/vitessce/widget_plugins/spatial_query.py @@ -24,9 +24,9 @@ const [uuid, setUuid] = React.useState(1); const [queryType, setQueryType] = React.useState('grid'); - const [maxDist, setMaxDist] = React.useState(100); - const [minSize, setMinSize] = React.useState(4); - const [minCount, setMinCount] = React.useState(10); + const [maxDist, setMaxDist] = React.useState(10); + const [minSize, setMinSize] = React.useState(0); + const [minCount, setMinCount] = React.useState(0); const [minSupport, setMinSupport] = React.useState(0.5); const cellTypeOfInterest = obsSetSelection?.length === 1 && obsSetSelection[0][0] === "Cell Type" @@ -52,21 +52,21 @@


@@ -160,6 +160,7 @@ def __init__(self, dataset='test', spatial_key=spatial_key, label_key=label_key, + feature_name=feature_name, leaf_size=10, build_gene_index=False, if_lognorm=if_lognorm, @@ -171,7 +172,7 @@ def __init__(self, "version": "0.1.3", "tree": [ { - "name": "Spatial-Query Results", + "name": "SpatialQuery Results", "children": [ ] @@ -186,7 +187,7 @@ def __init__(self, }, { "color": [255, 255, 255], - "path": ["Spatial-Query Results"], + "path": ["SpatialQuery Results"], } ] @@ -218,7 +219,7 @@ def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): "version": "0.1.3", "tree": [ { - "name": f"Spatial-Query Results {sq_id}", + "name": f"SpatialQuery Results {sq_id}", "children": [ ] @@ -233,7 +234,7 @@ def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): motif = row["itemsets"] except KeyError: motif = row["motifs"] - cell_i = row["cell_id"] + cell_i = row["neighbor_id"] motif_name = str(list(motif)) @@ -248,8 +249,9 @@ def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): ] }) + first_ct_color = self.ct_to_color.get(list(motif)[0], [255, 255, 255]) obs_set_color.append({ - "color": [255, 255, 255], + "color": first_ct_color, "path": [additional_obs_sets["tree"][0]["name"], motif_name] }) @@ -282,7 +284,7 @@ def run_sq(self, prev_config): min_support=min_support, # dis_duplicates=dis_duplicates, if_display=True, - fig_size=(9, 6), + figsize=(9, 6), return_cellID=True, ) print(params_dict) From 9d817be22cd41947dbdaa0d38ceb681a639272c6 Mon Sep 17 00:00:00 2001 From: sa3520 Date: Mon, 2 Mar 2026 18:33:23 -0500 Subject: [PATCH 3/8] add query type with specified anchor cells --- src/vitessce/widget_plugins/spatial_query.py | 245 +++++++++++-------- 1 file changed, 146 insertions(+), 99 deletions(-) diff --git a/src/vitessce/widget_plugins/spatial_query.py b/src/vitessce/widget_plugins/spatial_query.py index 56117c83..d0d779ff 100644 --- a/src/vitessce/widget_plugins/spatial_query.py +++ b/src/vitessce/widget_plugins/spatial_query.py @@ -1,10 +1,14 @@ +import colorsys +import json from oxc_py import transform from ..widget import VitesscePlugin -PLUGIN_ESM = transform(""" -function createPlugins(utilsForPlugins) { - const { +def _build_plugin_esm(cell_type_list): + ct_list_js = json.dumps(cell_type_list) + js_source = f""" +function createPlugins(utilsForPlugins) {{ + const {{ React, PluginFileType, PluginViewType, @@ -12,120 +16,140 @@ PluginJointFileType, z, useCoordination, - } = utilsForPlugins; - function SpatialQueryView(props) { - const { coordinationScopes } = props; - const [{ + }} = utilsForPlugins; + + const CELL_TYPE_LIST = {ct_list_js}; + + function SpatialQueryView(props) {{ + const {{ coordinationScopes }} = props; + const [{{ queryParams, - obsSetSelection, - }, { + }}, {{ setQueryParams, - }] = useCoordination(['queryParams', 'obsSetSelection', 'obsType'], coordinationScopes); + }}] = useCoordination(['queryParams', 'obsType'], coordinationScopes); const [uuid, setUuid] = React.useState(1); const [queryType, setQueryType] = React.useState('grid'); + const [anchorCellType, setAnchorCellType] = React.useState(CELL_TYPE_LIST[0] ?? ''); const [maxDist, setMaxDist] = React.useState(10); - const [minSize, setMinSize] = React.useState(0); - const [minCount, setMinCount] = React.useState(0); const [minSupport, setMinSupport] = React.useState(0.5); + const [k, setK] = React.useState(20); + const [nPoints, setNPoints] = React.useState(1000); + + const isAnchorMode = queryType === 'anchor-type-knn' || queryType === 'anchor-type-dist'; - const cellTypeOfInterest = obsSetSelection?.length === 1 && obsSetSelection[0][0] === "Cell Type" - ? obsSetSelection[0][1] - : null; + const onQueryTypeChange = React.useCallback((e) => {{ + const newType = e.target.value; + setQueryType(newType); + // Update maxDist default when switching modes + if (newType === 'anchor-type-knn') {{ + setMaxDist(20); + }} else {{ + setMaxDist(10); + }} + }}, []); - const onQueryTypeChange = React.useCallback((e) => { - setQueryType(e.target.value); - }, []); + const radiusLabel = queryType === 'anchor-type-knn' ? 'Max. Dist.' : 'Radius'; return (
-

Spatial Query Manager

+

SpatialQuery Manager


+ {{isAnchorMode && ( -
+ )}} + {{isAnchorMode &&
}}
+ {{queryType === 'anchor-type-knn' && ( -
+ )}} + {{queryType === 'rand' && ( + + )}}
- {/* TODO: disDuplicates: Distinguish duplicates in patterns. */} - + }}}}>Find patterns
); - } + }} const pluginCoordinationTypes = [ - new PluginCoordinationType('queryParams', null, z.object({ + new PluginCoordinationType('queryParams', null, z.object({{ cellTypeOfInterest: z.string().nullable(), - queryType: z.enum(['grid', 'rand', 'ct-center']), + queryType: z.enum(['grid', 'rand', 'anchor-type-knn', 'anchor-type-dist']), maxDist: z.number(), - minSize: z.number(), - minCount: z.number(), minSupport: z.number(), - disDuplicates: z.boolean(), + k: z.number(), + nPoints: z.number(), uuid: z.number(), - }).partial().nullable()), + }}).partial().nullable()), ]; const pluginViewTypes = [ - new PluginViewType('spatialQuery', SpatialQueryView, ['queryParams', 'obsSetSelection', 'obsType']), + new PluginViewType('spatialQuery', SpatialQueryView, ['queryParams', 'obsType']), ]; - return { pluginViewTypes, pluginCoordinationTypes }; -} -export default { createPlugins }; -""") + return {{ pluginViewTypes, pluginCoordinationTypes }}; +}} +export default {{ createPlugins }}; +""" + return transform(js_source) class SpatialQueryPlugin(VitesscePlugin): """ Spatial-Query plugin view renders controls to change parameters passed to the Spatial-Query methods. """ - plugin_esm = PLUGIN_ESM commands = {} - def __init__(self, - adata, - spatial_key="X_spatial", + def __init__(self, + adata, + spatial_key="X_spatial", label_key="cell_type", feature_name="gene_name", if_lognorm=True, @@ -156,10 +180,10 @@ def __init__(self, self.label_key = label_key self.tt = spatial_query( - adata=adata, - dataset='test', - spatial_key=spatial_key, - label_key=label_key, + adata=adata, + dataset='test', + spatial_key=spatial_key, + label_key=label_key, feature_name=feature_name, leaf_size=10, build_gene_index=False, @@ -168,14 +192,19 @@ def __init__(self, self.tab20_rgb = [[int(r * 255), int(g * 255), int(b * 255)] for (r, g, b, a) in [plt.cm.tab20(i) for i in range(20)]] + cell_type_list = adata.obs[label_key].unique().tolist() + self.cell_type_list = cell_type_list + self.initial_query_params = {} + + # Build ESM with cell type list embedded directly as a JS constant + self.plugin_esm = _build_plugin_esm(cell_type_list) + self.additional_obs_sets = { "version": "0.1.3", "tree": [ { "name": "SpatialQuery Results", - "children": [ - - ] + "children": [] } ] } @@ -228,13 +257,18 @@ def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): } obs_set_color = [] + n_motifs = len(fp_tree) - for row_i, row in fp_tree.iterrows(): + for motif_i, (row_i, row) in enumerate(fp_tree.iterrows()): try: motif = row["itemsets"] except KeyError: motif = row["motifs"] - cell_i = row["neighbor_id"] + # anchor-type queries: use neighbor_id for motif cells, grid/rand use cell_id + if "neighbor_id" in row.index: + cell_i = row["neighbor_id"] + else: + cell_i = row["cell_id"] motif_name = str(list(motif)) @@ -249,9 +283,12 @@ def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): ] }) - first_ct_color = self.ct_to_color.get(list(motif)[0], [255, 255, 255]) + # Assign each motif a unique color by evenly spacing hues around the color wheel + hue = motif_i / max(n_motifs, 1) + r, g, b = colorsys.hls_to_rgb(hue, 0.55, 0.75) + motif_color = [int(r * 255), int(g * 255), int(b * 255)] obs_set_color.append({ - "color": first_ct_color, + "color": motif_color, "path": [additional_obs_sets["tree"][0]["name"], motif_name] }) @@ -267,45 +304,50 @@ def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): def run_sq(self, prev_config): query_params = prev_config["coordinationSpace"]["queryParams"]["A"] - max_dist = query_params.get("maxDist", 150) - min_size = query_params.get("minSize", 4) - # min_count = query_params.get("minCount", 10) + max_dist = query_params.get("maxDist", 10) min_support = query_params.get("minSupport", 0.5) - # dis_duplicates = query_params.get("disDuplicates", False) # if distinguish duplicates of cell types in neighborhood + k = query_params.get("k", 20) + n_points = query_params.get("nPoints", 1000) query_type = query_params.get("queryType", "grid") cell_type_of_interest = query_params.get("cellTypeOfInterest", None) query_uuid = query_params["uuid"] - params_dict = dict( - max_dist=max_dist, - min_size=min_size, - # min_count=min_count, - min_support=min_support, - # dis_duplicates=dis_duplicates, - if_display=True, - figsize=(9, 6), - return_cellID=True, - ) - print(params_dict) - - # TODO: add unit tests for this functionality + print(query_params) if query_type == "rand": - # TODO: implement param similar to return_grid for find_patterns_rand (to return the random points used) - fp_tree = self.tt.find_patterns_rand(**params_dict) + fp_tree = self.tt.find_patterns_rand( + max_dist=max_dist, + n_points=int(n_points), + min_support=min_support, + if_display=True, + figsize=(9, 6), + return_cellID=True, + ) elif query_type == "grid": - params_dict["return_grid"] = True - fp_tree, grid_pos = self.tt.find_patterns_grid(**params_dict) - elif query_type == "ct-center": + fp_tree, grid_pos = self.tt.find_patterns_grid( + max_dist=max_dist, + min_support=min_support, + if_display=True, + figsize=(9, 6), + return_cellID=True, + return_grid=True, + ) + elif query_type == "anchor-type-knn": fp_tree = self.tt.motif_enrichment_knn( ct=cell_type_of_interest, - k=20, # TODO: make this a parameter in the UI. + k=int(k), + max_dist=max_dist, + min_support=min_support, + return_cellID=True, + ) + elif query_type == "anchor-type-dist": + fp_tree = self.tt.motif_enrichment_dist( + ct=cell_type_of_interest, + max_dist=max_dist, min_support=min_support, - # dis_duplicates=dis_duplicates, return_cellID=True, ) - print(fp_tree) # TODO: implement query types that are dependent on motif selection. @@ -316,7 +358,12 @@ def run_sq(self, prev_config): # Perform query (new_additional_obs_sets, new_obs_set_color) = self.fp_tree_to_obs_sets_tree(fp_tree, query_uuid) - additional_obs_sets["tree"][0] = new_additional_obs_sets["tree"][0] + new_sq_node = new_additional_obs_sets["tree"][0] + sq_idx = next((i for i, n in enumerate(additional_obs_sets["tree"]) if n["name"].startswith("SpatialQuery Results")), None) + if sq_idx is not None: + additional_obs_sets["tree"][sq_idx] = new_sq_node + else: + additional_obs_sets["tree"].append(new_sq_node) prev_config["coordinationSpace"]["additionalObsSets"]["A"] = additional_obs_sets obs_set_color += new_obs_set_color From 3a961306a336ddaf4dd176fedb5286cad96e00d1 Mon Sep 17 00:00:00 2001 From: sa3520 Date: Tue, 3 Mar 2026 11:15:28 -0500 Subject: [PATCH 4/8] seperate colors to by motif and by type in two trees --- src/vitessce/widget_plugins/spatial_query.py | 122 +++++++++---------- 1 file changed, 59 insertions(+), 63 deletions(-) diff --git a/src/vitessce/widget_plugins/spatial_query.py b/src/vitessce/widget_plugins/spatial_query.py index d0d779ff..9e0c8b7f 100644 --- a/src/vitessce/widget_plugins/spatial_query.py +++ b/src/vitessce/widget_plugins/spatial_query.py @@ -201,12 +201,7 @@ def __init__(self, self.additional_obs_sets = { "version": "0.1.3", - "tree": [ - { - "name": "SpatialQuery Results", - "children": [] - } - ] + "tree": [] } self.obs_set_color = [ @@ -214,10 +209,6 @@ def __init__(self, "color": [255, 255, 255], "path": ["Cell Type"], }, - { - "color": [255, 255, 255], - "path": ["SpatialQuery Results"], - } ] self.ct_to_color = dict() @@ -244,61 +235,65 @@ def get_matching_cell_ids(self, cell_type, cell_i): return matches def fp_tree_to_obs_sets_tree(self, fp_tree, sq_id): - additional_obs_sets = { - "version": "0.1.3", - "tree": [ - { - "name": f"SpatialQuery Results {sq_id}", - "children": [ + sq_motif_name = f"SpatialQuery Results {sq_id} — By Motif" + sq_ct_name = f"SpatialQuery Results {sq_id} — By Cell Type" - ] - } - ] - } - - obs_set_color = [] - n_motifs = len(fp_tree) + # Pass 1: collect per-motif data and accumulate deduped cell ids per cell type + motif_rows = [] + ct_to_cell_ids = {} - for motif_i, (row_i, row) in enumerate(fp_tree.iterrows()): + for _, row in fp_tree.iterrows(): try: motif = row["itemsets"] except KeyError: motif = row["motifs"] - # anchor-type queries: use neighbor_id for motif cells, grid/rand use cell_id - if "neighbor_id" in row.index: - cell_i = row["neighbor_id"] - else: - cell_i = row["cell_id"] - - motif_name = str(list(motif)) + cell_i = row["neighbor_id"] if "neighbor_id" in row.index else row["cell_id"] + motif_rows.append((motif, cell_i)) + for cell_type in motif: + matching = {i for i in cell_i if self.cell_id_to_cell_type.get(self.cell_i_to_cell_id.get(i)) == cell_type} + ct_to_cell_ids.setdefault(cell_type, set()).update(matching) - additional_obs_sets["tree"][0]["children"].append({ - "name": motif_name, - "children": [ - { - "name": cell_type, - "set": self.get_matching_cell_ids(cell_type, cell_i) - } - for cell_type in motif - ] - }) + n_motifs = len(motif_rows) + obs_set_color = [] - # Assign each motif a unique color by evenly spacing hues around the color wheel + # Node 1: "By Motif" — each motif is a leaf, colored by motif index + by_motif_children = [] + for motif_i, (motif, cell_i) in enumerate(motif_rows): + motif_name = str(list(motif)) hue = motif_i / max(n_motifs, 1) r, g, b = colorsys.hls_to_rgb(hue, 0.55, 0.75) motif_color = [int(r * 255), int(g * 255), int(b * 255)] - obs_set_color.append({ - "color": motif_color, - "path": [additional_obs_sets["tree"][0]["name"], motif_name] + motif_cell_types = set(motif) + motif_cell_ids = list({ + i for i in cell_i + if self.cell_id_to_cell_type.get(self.cell_i_to_cell_id.get(i)) in motif_cell_types + }) + by_motif_children.append({ + "name": motif_name, + "set": [[self.cell_i_to_cell_id[i], None] for i in motif_cell_ids if i in self.cell_i_to_cell_id] + }) + obs_set_color.append({"color": motif_color, "path": [sq_motif_name, motif_name]}) + + # Node 2: "By Cell Type" — each cell type is a leaf (union across motifs), colored by cell type + by_ct_children = [] + for cell_type, cell_ids_set in ct_to_cell_ids.items(): + by_ct_children.append({ + "name": cell_type, + "set": [[self.cell_i_to_cell_id[i], None] for i in sorted(cell_ids_set) if i in self.cell_i_to_cell_id] }) + obs_set_color.append({"color": self.ct_to_color[cell_type], "path": [sq_ct_name, cell_type]}) + + additional_obs_sets = { + "version": "0.1.3", + "tree": [ + {"name": sq_motif_name, "children": by_motif_children}, + {"name": sq_ct_name, "children": by_ct_children}, + ] + } + + obs_set_color.insert(0, {"color": [255, 255, 255], "path": [sq_motif_name]}) + obs_set_color.insert(0, {"color": [255, 255, 255], "path": [sq_ct_name]}) - for cell_type in motif: - color = self.ct_to_color[cell_type] - path = [additional_obs_sets["tree"][0]["name"], motif_name, cell_type] - obs_set_color.append({ - "color": color, - "path": path - }) return (additional_obs_sets, obs_set_color) def run_sq(self, prev_config): @@ -358,24 +353,25 @@ def run_sq(self, prev_config): # Perform query (new_additional_obs_sets, new_obs_set_color) = self.fp_tree_to_obs_sets_tree(fp_tree, query_uuid) - new_sq_node = new_additional_obs_sets["tree"][0] - sq_idx = next((i for i, n in enumerate(additional_obs_sets["tree"]) if n["name"].startswith("SpatialQuery Results")), None) - if sq_idx is not None: - additional_obs_sets["tree"][sq_idx] = new_sq_node - else: - additional_obs_sets["tree"].append(new_sq_node) + # Replace any existing SpatialQuery Results nodes (both By Motif and By Cell Type) + existing_tree = additional_obs_sets["tree"] + existing_tree = [n for n in existing_tree if not n["name"].startswith("SpatialQuery Results")] + existing_tree += new_additional_obs_sets["tree"] + additional_obs_sets["tree"] = existing_tree prev_config["coordinationSpace"]["additionalObsSets"]["A"] = additional_obs_sets obs_set_color += new_obs_set_color prev_config["coordinationSpace"]["obsSetColor"]["A"] = obs_set_color - motif_to_select = new_additional_obs_sets["tree"][0]["children"][0]["name"] - new_obs_set_selection = [[new_additional_obs_sets["tree"][0]["name"], motif_to_select, node["name"]] for node in new_additional_obs_sets["tree"][0]["children"][0]["children"]] + # Default selection: all motif leaf nodes under "By Motif" node + sq_motif_node = new_additional_obs_sets["tree"][0] # "...By Motif" + new_obs_set_selection = [ + [sq_motif_node["name"], motif["name"]] + for motif in sq_motif_node["children"] + ] prev_config["coordinationSpace"]["obsSetSelection"]["A"] = new_obs_set_selection - # TODO: need to fix bug that prevents this from working - # Reference: https://github.com/vitessce/vitessce/blob/774328ab5c4436576dd2e8e4fff0758d6c6cce89/packages/view-types/obs-sets-manager/src/ObsSetsManagerSubscriber.js#L104 - prev_config["coordinationSpace"]["obsSetExpansion"]["A"] = [path[:-1] for path in new_obs_set_selection] + prev_config["coordinationSpace"]["obsSetExpansion"]["A"] = [[sq_motif_node["name"]]] return {**prev_config, "uid": f"with_query_{query_uuid}"} From 13ad6483d65a648a476563254d82029e650c831c Mon Sep 17 00:00:00 2001 From: sa3520 Date: Tue, 3 Mar 2026 16:11:20 -0500 Subject: [PATCH 5/8] add HeatmapView of SpatialQuery motif --- src/vitessce/widget_plugins/spatial_query.py | 187 +++++++++++++++++-- 1 file changed, 173 insertions(+), 14 deletions(-) diff --git a/src/vitessce/widget_plugins/spatial_query.py b/src/vitessce/widget_plugins/spatial_query.py index 9e0c8b7f..653a1bdf 100644 --- a/src/vitessce/widget_plugins/spatial_query.py +++ b/src/vitessce/widget_plugins/spatial_query.py @@ -1,4 +1,6 @@ +import base64 import colorsys +import io import json from oxc_py import transform from ..widget import VitesscePlugin @@ -16,6 +18,7 @@ def _build_plugin_esm(cell_type_list): PluginJointFileType, z, useCoordination, + invokeCommand, }} = utilsForPlugins; const CELL_TYPE_LIST = {ct_list_js}; @@ -75,20 +78,20 @@ def _build_plugin_esm(cell_type_list): )}} {{isAnchorMode &&
}} - -
{{queryType === 'anchor-type-knn' && ( )}} + +
{{queryType === 'rand' && (