Aviti Sample Sheet Example
This example demonstrates how to parse an Aviti sample sheet (Sequencing Manifest) using elsheeto and extract key information including settings, run values, and sample details with composite index handling.
Sample Sheet Format
Aviti sample sheets (Sequencing Manifests) are organized into sections:
[Settings]: Sequencing configuration parameters
[RunValues]: Custom experiment metadata (optional)
[Samples]: Sample information with indexing details
A key feature of Aviti sample sheets is support for composite indices using + separators (e.g., ATGT+CGCT).
Example Script
Here’s the complete example script (examples/read_aviti.py):
1#!/usr/bin/env python3
2"""Example: Reading an Aviti sample sheet (Sequencing Manifest).
3
4This example demonstrates how to parse an Aviti sample sheet and extract
5key information including settings, run values, and sample details with
6composite index handling.
7
8Usage:
9 python examples/read_aviti.py
10 # or with uv:
11 uv run python examples/read_aviti.py
12"""
13
14from pathlib import Path
15
16from elsheeto import parse_aviti
17
18
19def main():
20 """Parse Aviti sample sheet and display key information."""
21 # Path to the example Aviti sample sheet
22 sample_sheet_path = Path(__file__).parent / "aviti_example1.csv"
23
24 # Get relative path from current working directory for display
25 try:
26 relative_path = sample_sheet_path.relative_to(Path.cwd())
27 except ValueError:
28 # Fallback to absolute path if relative path can't be computed
29 relative_path = sample_sheet_path
30
31 print(f"Reading Aviti sample sheet: {relative_path}")
32 print("=" * 65)
33
34 # Parse the sample sheet using elsheeto's facade function
35 try:
36 sample_sheet = parse_aviti(sample_sheet_path)
37 except Exception as e:
38 print(f"Error parsing sample sheet: {e}")
39 return
40
41 # Display run values information
42 if sample_sheet.run_values and sample_sheet.run_values.data:
43 print("RUN VALUES:")
44 print("-" * 35)
45 for key, value in sample_sheet.run_values.data.items():
46 print(f" {key}: {value}")
47 print()
48
49 # Display settings information
50 if sample_sheet.settings and sample_sheet.settings.data:
51 print("SETTINGS:")
52 print("-" * 35)
53
54 # Group settings by category for better readability
55 sequencing_settings = {}
56 adapter_settings = {}
57 other_settings = {}
58
59 for key, value in sample_sheet.settings.data.items():
60 if any(x in key.lower() for x in ["mask", "fastq", "mismatch", "umi"]):
61 sequencing_settings[key] = value
62 elif any(x in key.lower() for x in ["adapter", "trim"]):
63 adapter_settings[key] = value
64 else:
65 other_settings[key] = value
66
67 if other_settings:
68 print(" General Settings:")
69 for key, value in other_settings.items():
70 print(f" {key}: {value}")
71 print()
72
73 if sequencing_settings:
74 print(" Sequencing Configuration:")
75 for key, value in sequencing_settings.items():
76 print(f" {key}: {value}")
77 print()
78
79 if adapter_settings:
80 print(" Adapter Settings:")
81 for key, value in adapter_settings.items():
82 print(f" {key}: {value}")
83 print()
84
85 # Display sample information
86 print("SAMPLE INFORMATION:")
87 print("-" * 35)
88 print(f"Total samples: {len(sample_sheet.samples)}")
89 print()
90
91 # Categorize samples
92 phix_samples = [s for s in sample_sheet.samples if "phix" in s.sample_name.lower()]
93 regular_samples = [s for s in sample_sheet.samples if "phix" not in s.sample_name.lower()]
94
95 # Display PhiX control samples
96 if phix_samples:
97 print("PhiX Control Samples:")
98 for i, sample in enumerate(phix_samples, 1):
99 print(f" {i:2d}. Sample Name: {sample.sample_name}")
100 print(f" Index1: {sample.index1}")
101 print(f" Index2: {sample.index2}")
102 if sample.lane:
103 print(f" Lane: {sample.lane}")
104 if sample.project:
105 print(f" Project: {sample.project}")
106
107 # Handle composite indices
108 if "+" in sample.index1:
109 index1_parts = sample.index1.split("+")
110 print(f" Index1 composite parts: {' + '.join(index1_parts)}")
111 if "+" in sample.index2:
112 index2_parts = sample.index2.split("+")
113 print(f" Index2 composite parts: {' + '.join(index2_parts)}")
114 print()
115
116 # Display regular samples
117 if regular_samples:
118 print("Regular Samples:")
119 for i, sample in enumerate(regular_samples, 1):
120 print(f" {i:2d}. Sample Name: {sample.sample_name}")
121 print(f" Index1: {sample.index1}")
122 print(f" Index2: {sample.index2}")
123 if sample.lane:
124 print(f" Lane: {sample.lane}")
125 if sample.project:
126 print(f" Project: {sample.project}")
127 if sample.external_id:
128 print(f" External ID: {sample.external_id}")
129 if sample.description:
130 print(f" Description: {sample.description}")
131
132 # Handle composite indices
133 if "+" in sample.index1:
134 index1_parts = sample.index1.split("+")
135 print(f" Index1 composite parts: {' + '.join(index1_parts)}")
136 if "+" in sample.index2:
137 index2_parts = sample.index2.split("+")
138 print(f" Index2 composite parts: {' + '.join(index2_parts)}")
139 print()
140
141 # Display summary statistics
142 print("SUMMARY STATISTICS:")
143 print("-" * 35)
144
145 # Lane analysis
146 lanes_used = set()
147 for sample in sample_sheet.samples:
148 if sample.lane:
149 # Handle lane ranges like "1+2"
150 if "+" in sample.lane:
151 lanes_used.update(sample.lane.split("+"))
152 else:
153 lanes_used.add(sample.lane)
154
155 if lanes_used:
156 print(f"Lanes used: {', '.join(sorted(lanes_used))}")
157
158 # Index analysis
159 dual_indexed_samples = [s for s in sample_sheet.samples if s.index2]
160 composite_index1_samples = [s for s in sample_sheet.samples if "+" in s.index1]
161 composite_index2_samples = [s for s in sample_sheet.samples if "+" in s.index2]
162
163 print(f"PhiX control samples: {len(phix_samples)}")
164 print(f"Regular samples: {len(regular_samples)}")
165 print(f"Dual-indexed samples: {len(dual_indexed_samples)}")
166 print(f"Samples with composite Index1: {len(composite_index1_samples)}")
167 print(f"Samples with composite Index2: {len(composite_index2_samples)}")
168
169 # Project analysis
170 projects = {s.project for s in sample_sheet.samples if s.project}
171 if projects:
172 projects.discard("") # Remove empty strings
173 if projects:
174 print(f"Projects: {', '.join(sorted(projects))}")
175
176
177if __name__ == "__main__":
178 main()
Key Features Demonstrated
Import and Parsing
The script uses the simple facade function:
from elsheeto import parse_aviti
sample_sheet = parse_aviti(sample_sheet_path)
This single function call handles the entire three-stage parsing process.
Run Values Access
Aviti sheets can contain custom experiment metadata:
if sample_sheet.run_values and sample_sheet.run_values.data:
for key, value in sample_sheet.run_values.data.items():
print(f" {key}: {value}")
Settings Organization
The script categorizes settings for better readability:
# Group settings by category
sequencing_settings = {}
adapter_settings = {}
other_settings = {}
for key, value in sample_sheet.settings.data.items():
if any(x in key.lower() for x in ['mask', 'fastq', 'mismatch', 'umi']):
sequencing_settings[key] = value
elif any(x in key.lower() for x in ['adapter', 'trim']):
adapter_settings[key] = value
else:
other_settings[key] = value
Sample Categorization
The script intelligently categorizes samples:
# Categorize samples
phix_samples = [s for s in sample_sheet.samples if "phix" in s.sample_name.lower()]
regular_samples = [s for s in sample_sheet.samples if "phix" not in s.sample_name.lower()]
Composite Index Handling
A unique feature of Aviti is composite index support:
# Handle composite indices
if "+" in sample.index1:
index1_parts = sample.index1.split("+")
print(f" Index1 composite parts: {' + '.join(index1_parts)}")
Lane Range Processing
Aviti supports lane ranges like 1+2:
# Handle lane ranges like "1+2"
if "+" in sample.lane:
lanes_used.update(sample.lane.split("+"))
else:
lanes_used.add(sample.lane)
Sample Data File
The example uses aviti_example1.csv:
1[SETTINGS],,,,
2SettingName,Value,Lane,,
3SpikeInAsUnassigned,FALSE,,,
4# Lines can be commented out with a leading # sign. ,,,,
5# Read mask is set to all cycles less the last imaged (which is used for calculations),,,,
6R1FastQMask,R1:Y*N,1+2,,
7R2FastQMask,R2:Y*N,1+2,,
8,,,,
9# Index mask is set to index length with no FASTQ generated for Lanes 1 and 2.,,,,
10I1Mask,I1:Y*,1+2,,
11I1Fastq,FALSE,1+2,,
12I1MismatchThreshold,1,1+2,,
13I2Mask,I2:Y*,1+2,,
14I2Fastq,FALSE,1+2,,
15I2MismatchThreshold,1,1+2,,
16,,,,
17# UMI mask is set to nothing with no FASTQ generated for Lanes 1 and 2.,,,,
18UmiMask,I1:N*, 1+2,,
19UmiFastQ,FALSE,1+2,,
20,,,,
21# You can optionally trim adapters. The Elevate adapter is known so no need to specify. In this template adapter trimming is turned off. ,,,,
22AdapterTrimType, Paired-End, 1+2,,
23,,,,
24# Define the Read 1 Adapter sequence associated with the Adept Preparation Workflow & adapter trimming settings. An example Adapter value is provided below.,,,,
25R1Adapter,,1+2,,
26R1AdapterTrim,FALSE,1+2,,
27R1AdapterNMask,FALSE,1+2,,
28,,,,
29# Define the Read 2 Adapter sequence associated with the Adept Preparation Workflow & adapter trimming settings. An example Adapter value is provided below.,,,,
30R2Adapter,,1+2,,
31R2AdapterTrim,FALSE,1+2,,
32R2AdapterNMask,FALSE,1+2,,
33#Please visit https://docs.elembio.io/docs/run-manifest/settings/#adapter-trimming-settings for additional adapter trimming settings,,,,
34,,,,
35[RunValues],,,,
36Keyname, Value,,,
37Example-Custom-Key,Additional metadata can be added as a key-value pair.,,,
38,,,,
39[SAMPLES],,,,
40SampleName,Index1,Index2,Lane,Project
41# Make sure that in the Index1 and Index2 columns the index sequences for each library is in the same orientation (samples and PhiX controls).,,,,
42PhiX,ATGTCGCTAG,CTAGCTCGTA,1+2,
43PhiX,CACAGATCGT,ACGAGAGTCT,1+2,
44PhiX,GCACATAGTC,GACTACTAGC,1+2,
45PhiX,TGTGTCGACA,TGTCTGACAG,1+2,
46,,,,
47# Fill in the correct sample schema associated with the Adept Preparation Workflow for all sequenced samples. For example:,,,,
48ExampleSample_1,AAAAAAAAAA,GGGGGGGGGG,1+2,
49ExampleSample_2,TTTTTTTTTT,CCCCCCCCCC,1+2,
Expected Output
When you run the example script, you should see output like this:
Reading Aviti sample sheet: examples/aviti_example1.csv
=================================================================
RUN VALUES:
-----------------------------------
Keyname: Value
Example-Custom-Key: Additional metadata can be added as a key-value pair.
SETTINGS:
-----------------------------------
General Settings:
SpikeInAsUnassigned: FALSE
Sequencing Configuration:
R1FastQMask: R1:Y*N
R2FastQMask: R2:Y*N
I1Mask: I1:Y*
I1Fastq: FALSE
I1MismatchThreshold: 1
I2Mask: I2:Y*
I2Fastq: FALSE
I2MismatchThreshold: 1
UmiMask: I1:N*
UmiFastQ: FALSE
R1AdapterNMask: FALSE
R2AdapterNMask: FALSE
Adapter Settings:
AdapterTrimType: Paired-End
R1Adapter: 1+2
R1AdapterTrim: FALSE
R2Adapter: 1+2
R2AdapterTrim: FALSE
SAMPLE INFORMATION:
-----------------------------------
Total samples: 6
PhiX Control Samples:
1. Sample Name: PhiX
Index1: ATGTCGCTAG
Index2: CTAGCTCGTA
Lane: 1+2
2. Sample Name: PhiX
Index1: CACAGATCGT
Index2: ACGAGAGTCT
Lane: 1+2
3. Sample Name: PhiX
Index1: GCACATAGTC
Index2: GACTACTAGC
Lane: 1+2
4. Sample Name: PhiX
Index1: TGTGTCGACA
Index2: TGTCTGACAG
Lane: 1+2
Regular Samples:
1. Sample Name: ExampleSample_1
Index1: AAAAAAAAAA
Index2: GGGGGGGGGG
Lane: 1+2
2. Sample Name: ExampleSample_2
Index1: TTTTTTTTTT
Index2: CCCCCCCCCC
Lane: 1+2
SUMMARY STATISTICS:
-----------------------------------
Lanes used: 1, 2
PhiX control samples: 4
Regular samples: 2
Dual-indexed samples: 6
Samples with composite Index1: 0
Samples with composite Index2: 0
Key Output Sections
Run Values: Custom experiment metadata
Settings: Organized by category (General, Sequencing Configuration, Adapter Settings)
Sample Information: Separated into PhiX controls and regular samples
Summary Statistics: Analysis of indexing patterns and lane usage
The output shows:
6 total samples: 4 PhiX controls + 2 regular samples
Dual indexing: All samples have both Index1 and Index2
Lane configuration: All samples run on lanes 1+2
Organized settings: Grouped by function for easy reading
Advanced Features
Composite Index Example
Here’s how elsheeto handles composite indices. If you had a sample with:
SampleName,Index1,Index2
CompositeSample,ATGT+CGCT,GGAA+TTCC
The script would output:
Sample Name: CompositeSample
Index1: ATGT+CGCT
Index1 composite parts: ATGT + CGCT
Index2: GGAA+TTCC
Index2 composite parts: GGAA + TTCC
Validation Features
elsheeto automatically validates:
Index sequences: Ensures valid DNA sequences or alphanumeric IDs
Composite format: Validates
+separated composite indicesRequired fields: Ensures
SampleNameandIndex1are present
Error Handling
The script includes proper error handling:
try:
sample_sheet = parse_aviti(sample_sheet_path)
except Exception as e:
print(f"Error parsing sample sheet: {e}")
return
This ensures graceful handling of malformed files or validation errors.
Running the Example
You can run this example in several ways:
# Using uv (recommended)
uv run python examples/read_aviti.py
# Or generate output file
make examples
# Direct execution (if elsheeto is installed)
python examples/read_aviti.py
The example demonstrates elsheeto’s advanced features including composite index handling, intelligent sample categorization, and comprehensive settings analysis.
Modifying Aviti Sample Sheets
In addition to parsing, elsheeto provides powerful capabilities for modifying and writing Aviti sample sheets. This is useful for:
Adding new samples to existing experiments
Updating sample metadata (project names, descriptions, etc.)
Removing samples that failed quality control
Changing experimental parameters (run values, settings)
Quick Start: Simple Modifications
For simple modifications, use the fluent API:
from elsheeto import parse_aviti, write_aviti_to_file
from elsheeto.models.aviti import AvitiSample
# Load existing sheet
sheet = parse_aviti("experiment.csv")
# Make modifications using fluent API
modified_sheet = (sheet
.with_sample_added(AvitiSample(
sample_name="New_Sample",
index1="ATCGATCG",
project="NewProject"
))
.with_sample_modified("Old_Sample", project="UpdatedProject")
.with_run_value_added("ModificationDate", "2024-01-15")
)
# Write back to file
write_aviti_to_file(modified_sheet, "modified_experiment.csv")
Complex Modifications: Builder Pattern
For complex operations, use the builder pattern:
from elsheeto.models.aviti import AvitiSheetBuilder, AvitiSample
# Create new sheet from scratch or modify existing
builder = AvitiSheetBuilder() # or AvitiSheetBuilder.from_sheet(existing_sheet)
# Add multiple samples
samples = [
AvitiSample(sample_name=f"Sample_{i}", index1="ATCG", project="BatchProject")
for i in range(1, 10)
]
builder.add_samples(samples)
# Add run values and settings
builder.add_run_values({
"Experiment": "BATCH_001",
"Date": "2024-01-15"
})
builder.add_setting("ReadLength", "150")
# Build the immutable sheet
final_sheet = builder.build()
Modification Examples
See the comprehensive examples in:
examples/quick_start_aviti.py: Simple introduction to modification featuresexamples/modify_aviti.py: Complete guide with advanced patterns
Key modification features:
Immutable operations: All modifications return new objects, preserving originals
Fluent API: Chain multiple modifications for readable code
Batch operations: Add/remove multiple samples efficiently
Round-trip compatibility: Modified sheets parse correctly when re-read
Composite index support: Full support for
+separated indicesValidation: All modifications are validated according to Aviti format requirements
Available Modification Methods
Sample Operations:
with_sample_added(sample)- Add a new samplewith_sample_modified(name, **kwargs)- Update sample fieldswith_sample_removed(name)- Remove a sample by namewith_samples_filtered(predicate)- Keep only samples matching criteria
Run Values Operations:
with_run_value_added(key, value)- Add single run valuewith_run_values_updated(dict)- Add/update multiple run values
Settings Operations:
with_setting_added(name, value, lane=None)- Add setting entry
The modification system maintains full type safety and validation while providing both simple and advanced APIs for different use cases.