Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# openpipeline_qc x.x.x
# openpipeline_qc 0.2.0

## MINOR CHANGES

* `src/workflows/generate_qc_report`: Add min and max constraints to `--max_samples_per_report`, to limit the number of samples per report between 1 and 20 (PR #11).

* Update `openpipeline` dependency to v4.0.0 and `siqc` to version v0.2.0 (PR #15).

# openpipeline_qc 0.1.1

## MINOR CHANGES
Expand Down
2 changes: 1 addition & 1 deletion _viash.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repositories:
- name: openpipeline
repo: openpipeline
type: vsh
tag: v3.0.0
tag: v4.0.0
- name: craftbox
repo: craftbox
type: vsh
Expand Down
8 changes: 8 additions & 0 deletions resources_test_scripts/qc_sample_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ viash run src/ingestion_qc/h5mu_to_qc_json/config.vsh.yaml --engine docker -- \
--output "$OUT_DIR_SPATIAL"/xenium_dataset.json \
--output_reporting_json "$OUT_DIR_SPATIAL"/xenium_report_structure.json

viash run src/ingestion_qc/h5mu_to_qc_json/config.vsh.yaml --engine docker -- \
--input "$OUT_DIR_SPATIAL"/visium_tiny.qc.h5mu \
--input "$OUT_DIR_SPATIAL"/visium_tiny.qc.h5mu \
--ingestion_method visium \
--min_num_nonzero_vars 1 \
--output "$OUT_DIR_SPATIAL"/visium_dataset.json \
--output_reporting_json "$OUT_DIR_SPATIAL"/visium_report_structure.json

# remove all state yaml files
rm "$OUT_DIR"/*.yaml
rm "$OUT_DIR_SPATIAL"/*.yaml
Expand Down
2 changes: 2 additions & 0 deletions resources_test_scripts/spatial_qc_sample_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ param_list:
input: s3://openpipelines-bio/openpipeline_spatial/resources_test/xenium/xenium_tiny.h5mu
- id: Lung5_Rep2_tiny
input: s3://openpipelines-bio/openpipeline_spatial/resources_test/cosmx/Lung5_Rep2_tiny.h5mu
- id: visium_tiny
input: s3://openpipelines-bio/openpipeline_spatial/resources_test/visium/visium_tiny.h5mu
Comment thread
jakubmajercik marked this conversation as resolved.
Outdated
var_name_mitochondrial_genes: mitochondrial
var_name_ribosomal_genes: ribosomal
output: '\$id.qc.h5mu'
Expand Down
5 changes: 2 additions & 3 deletions src/ingestion_qc/generate_html/config.vsh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ engines:
- git
- type: docker
run: |
npm install -g pnpm@latest-10 \
&& cd /opt && git clone -b v0.1.0 https://github.com/openpipelines-bio/siqc.git \
&& cd siqc && pnpm install \
cd /opt && git clone -b v0.2.0 https://github.com/openpipelines-bio/siqc.git \
&& cd siqc && npm install \
&& true

runners:
Expand Down
12 changes: 1 addition & 11 deletions src/ingestion_qc/generate_html/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,4 @@ ABSOLUTE_OUTPUT=$(realpath $par_output_qc_report)
cd /opt/siqc
mkdir src/data

echo "Compressing input data..."
pnpm run compress_data "$ABSOLUTE_INPUT_DATA" "src/data/dataset.ts"

echo "Compressing report structure..."
pnpm run compress_data "$ABSOLUTE_INPUT_STRUCTURE" "src/data/report_structure.ts"

echo "Generating HTML..."
pnpm run build

echo "Copying HTML to output directory..."
cp dist/index.html "$ABSOLUTE_OUTPUT"
npm run cli render -- --data "$ABSOLUTE_INPUT_DATA" --structure "$ABSOLUTE_INPUT_STRUCTURE" --output "$ABSOLUTE_OUTPUT"
Comment thread
jakubmajercik marked this conversation as resolved.
1 change: 1 addition & 0 deletions src/ingestion_qc/h5mu_to_qc_json/config.vsh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ argument_groups:
choices:
- cellranger_multi
- xenium
- visium
description: Method that was used to ingest the data - this will define the structure of the report that is generated.
- name: --obs_sample_id
type: string
Expand Down
67 changes: 67 additions & 0 deletions src/ingestion_qc/h5mu_to_qc_json/report_structure/visium.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"categories": [
{
"name": "Sample QC",
"key": "sample_summary_stats",
"additionalAxes": false,
"defaultFilters": []
},
{
"name": "Cell RNA QC",
"key": "cell_rna_stats",
"additionalAxes": true,
"defaultFilters": [
{
"type": "histogram",
"visualizationType": "histogram",
"field": "total_counts",
"label": "Total UMI per cell",
"description": "Total number of RNA molecules detected per cell. Low values typically indicate empty droplets or low-quality cells that should be filtered out.",
"cutoffMin": null,
"cutoffMax": null,
"zoomMax": null,
"nBins": 50,
"groupBy": "sample_id",
"yAxisType": "linear"
},
{
"type": "histogram",
"visualizationType": "histogram",
"field": "num_nonzero_vars",
"label": "Number of non-zero genes per cell",
"description": "Count of unique genes detected in each cell. Low gene counts often indicate poor-quality cells.",
"cutoffMin": null,
"cutoffMax": null,
"zoomMax": null,
"nBins": 50,
"groupBy": "sample_id",
"yAxisType": "linear"
},
{
"type": "histogram",
"visualizationType": "histogram",
"field": "fraction_mitochondrial",
"label": "Fraction UMI of mitochondrial genes per cell",
"description": "Proportion of cell's RNA from mitochondrial genes.",
"cutoffMin": null,
"cutoffMax": null,
"nBins": 50,
"groupBy": "sample_id",
"yAxisType": "linear"
},
{
"type": "histogram",
"visualizationType": "histogram",
"field": "fraction_ribosomal",
"label": "Fraction UMI of ribosomal genes per cell",
"description": "Proportion of cell's RNA from ribosomal protein genes. Extreme values may indicate stress responses or cell cycle abnormalities.",
"cutoffMin": null,
"cutoffMax": null,
"nBins": 50,
"groupBy": "sample_id",
"yAxisType": "linear"
}
]
}
]
}
33 changes: 31 additions & 2 deletions src/ingestion_qc/h5mu_to_qc_json/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,31 @@ def generate_xenium_stats(mod_obs, sample_id, required_keys):
**{key: mod_obs[key] for key in metadata_obs_keys}
}
)

return cell_rna_stats

def generate_visium_stats(mod_obs, sample_id, required_keys):

# Format required columns
mod_obs = format_required_columns(required_keys, mod_obs)

# Format visium-specific columns
visium_formatted_columns = ["x_coord", "y_coord"]
for key in visium_formatted_columns:
mod_obs[key] = mod_obs[key].astype("float16")

# Fetch and format all categorical columns for grouping
metadata_obs_keys, mod_obs = format_categorical_columns(mod_obs)

# Create cell RNA stats dataframe
cell_rna_stats = pd.DataFrame(
{
"sample_id": pd.Categorical(sample_id),
**{key: mod_obs[key] for key in required_keys},
**{key: mod_obs[key] for key in visium_formatted_columns},
**{key: mod_obs[key] for key in metadata_obs_keys}
}
)

return cell_rna_stats

Expand Down Expand Up @@ -286,7 +311,7 @@ def main(par):
barcodes_original_count = mod_obs.shape[0]

# Add coordinates to obs before filtering
if par["ingestion_method"] == "xenium":
if par["ingestion_method"] == "xenium" or par["ingestion_method"] == "visium":
mod_obs["x_coord"] = mod_obsm["spatial"][:, 0]
mod_obs["y_coord"] = mod_obsm["spatial"][:, 1]

Expand Down Expand Up @@ -340,6 +365,9 @@ def main(par):

if par["ingestion_method"] == "xenium":
cell_rna_stats = generate_xenium_stats(mod_obs, sample_id, required_keys)

if par["ingestion_method"] == "visium":
cell_rna_stats = generate_visium_stats(mod_obs, sample_id, required_keys)

cell_stats_dfs.append(cell_rna_stats)
sample_stats_dfs.append(sample_summary_stats)
Expand Down Expand Up @@ -374,7 +402,8 @@ def main(par):

report_structures = {
"cellranger_multi": os.path.join(meta["resources_dir"], "report_structure/cellranger.json"),
"xenium": os.path.join(meta["resources_dir"], "report_structure/xenium.json")
"xenium": os.path.join(meta["resources_dir"], "report_structure/xenium.json"),
"visium": os.path.join(meta["resources_dir"], "report_structure/visium.json")
}

logger.info(f"Writing output report structure json to {par['output_reporting_json']}")
Expand Down
10 changes: 10 additions & 0 deletions src/workflows/generate_qc_report/config.vsh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ argument_groups:
choices:
- cellranger_multi
- xenium
- visium
- name: --sample_metadata
type: file
required: false
Expand Down Expand Up @@ -131,6 +132,15 @@ test_resources:
- type: nextflow_script
path: test.nf
entrypoint: test_with_cellbender
- type: nextflow_script
path: test.nf
entrypoint: test_xenium
- type: nextflow_script
Comment thread
jakubmajercik marked this conversation as resolved.
Outdated
path: test.nf
entrypoint: test_cosmx
- type: nextflow_script
path: test.nf
entrypoint: test_visium

dependencies:
- name: metadata/add_id
Expand Down
14 changes: 14 additions & 0 deletions src/workflows/generate_qc_report/integration_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,17 @@ nextflow run . \
-entry test_multiple_reports \
-c src/configs/labels_ci.config \
-resume

nextflow run . \
-main-script src/workflows/generate_qc_report/test.nf \
-profile docker,no_publish,local \
Comment thread
jakubmajercik marked this conversation as resolved.
Outdated
-entry test_cosmx \
-c src/configs/labels_ci.config \
-resume

nextflow run . \
-main-script src/workflows/generate_qc_report/test.nf \
-profile docker,no_publish,local \
-entry test_visium \
-c src/configs/labels_ci.config \
-resume
46 changes: 45 additions & 1 deletion src/workflows/generate_qc_report/test.nf
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,48 @@ workflow test_multiple_reports {
assert files.size() == 5 : "Output directory should contain exactly 5 files, but found ${files.size()} files"
"Output: $output"
}
}
}
workflow test_visium {

resources_test_file = file(params.resources_test)

output_ch = Channel.fromList([
[
id: "sample_one",
input: resources_test_file.resolve("spatial_qc_sample_data/visium_tiny.qc.h5mu"),
run_cellbender: false,
ingestion_method: "visium",
min_num_nonzero_vars: "1",
output_html: "report.html",
publish_dir: "test_out"
],
[
id: "sample_two",
input: resources_test_file.resolve("spatial_qc_sample_data/visium_tiny.qc.h5mu"),
ingestion_method: "visium",
min_num_nonzero_vars: "1",
run_cellbender: false,
output_html: "report.html",
publish_dir: "test_out"
]
])

| map{ state -> [state.id, state] }
| generate_qc_report

| view { output ->
assert output.size() == 2 : "Outputs should contain two elements; [id, state]"
def id = output[0]
def state = output [1]
assert id == "combined": "Output ID should be `combined`"
assert state instanceof Map : "State should be a map. Found: ${state}"
assert state.containsKey("output_qc_report"): "Output should contain key `output_qc_report`"
assert state.containsKey("output_processed_h5mu"): "Output should contain key `output_processed_h5mu`"
assert state.output_qc_report.size() == 1 : "Expected exactly one output HTML file to be generated"
assert state.output_qc_report.every { it.isFile()} : "All output HTML report files should exist"
assert state.output_processed_h5mu.isDirectory() : "Output directory should exist"
def files = state.output_processed_h5mu.listFiles().findAll { it.isFile() }
assert files.size() == 2 : "Output directory should contain exactly 2 files, but found ${files.size()} files"
"Output: $output"
}
}