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

  1. Run Values: Custom experiment metadata

  2. Settings: Organized by category (General, Sequencing Configuration, Adapter Settings)

  3. Sample Information: Separated into PhiX controls and regular samples

  4. 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 indices

  • Required fields: Ensures SampleName and Index1 are 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 features

  • examples/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 indices

  • Validation: All modifications are validated according to Aviti format requirements

Available Modification Methods

Sample Operations:

  • with_sample_added(sample) - Add a new sample

  • with_sample_modified(name, **kwargs) - Update sample fields

  • with_sample_removed(name) - Remove a sample by name

  • with_samples_filtered(predicate) - Keep only samples matching criteria

Run Values Operations:

  • with_run_value_added(key, value) - Add single run value

  • with_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.