Aye10032's picture
download
raw
7.77 kB
import random
from contextlib import ExitStack
from pathlib import Path
from loguru import logger
from bioflow_sim.core.io import (
fastq_writer,
prepare_output_directory,
read_fasta,
validate_sample_name,
write_fastq_record,
write_json,
write_rows,
write_tsv,
)
from bioflow_sim.generators.random_values import sample_positive_normal
from bioflow_sim.generators.read_models import TECHNOLOGIES, introduce_errors
from bioflow_sim.generators.sequences import (
DNA_ALPHABET,
reverse_complement,
sample_genomic_template,
)
from bioflow_sim.generators.variants import apply_snvs, generate_snvs
DNA_TECHNOLOGY_NAMES = tuple(sorted(TECHNOLOGIES))
def simulate_dna(
*,
reference: Path,
output_dir: Path,
sample: str,
technology_name: str,
reads: int,
seed: int,
read_length: int | None,
fragment_mean: int,
fragment_sd: int,
long_read_mean: int | None,
long_read_sd: int | None,
snvs: int,
) -> dict[str, object]:
if reads < 1:
raise ValueError('reads must be at least 1')
validate_sample_name(sample)
technology = TECHNOLOGIES[technology_name]
records = read_fasta(reference)
contigs = [(name, sequence) for name, _, sequence in records]
usable = [(name, seq) for name, seq in contigs if any(b in DNA_ALPHABET for b in seq)]
if not usable:
raise ValueError('reference has no usable DNA sequences')
rng = random.Random(seed)
variants = generate_snvs(rng, usable, snvs)
simulation_sequences = apply_snvs(usable, variants)
prepare_output_directory(output_dir)
raw_dir = output_dir / 'raw'
truth_path = output_dir / 'truth' / 'reads.tsv'
truth_rows: list[list[object]] = []
logger.info('Loaded {} reference sequences from {}', len(usable), reference)
with ExitStack() as stack:
if technology.paired:
r1_path = raw_dir / f'{sample}_R1.fastq.gz'
r2_path = raw_dir / f'{sample}_R2.fastq.gz'
r1 = stack.enter_context(fastq_writer(r1_path))
r2 = stack.enter_context(fastq_writer(r2_path))
output_files = [str(r1_path.relative_to(output_dir)), str(r2_path.relative_to(output_dir))]
else:
read_path = raw_dir / f'{sample}.fastq.gz'
single = stack.enter_context(fastq_writer(read_path))
output_files = [str(read_path.relative_to(output_dir))]
for number in range(1, reads + 1):
if technology.paired:
length = read_length or technology.mean_length
eligible = [(name, seq) for name, seq in simulation_sequences if len(seq) >= length * 2]
if not eligible:
raise ValueError(f'no contig is long enough for paired reads of {length} bp')
max_fragment = max(len(seq) for _, seq in eligible)
fragment_length = sample_positive_normal(rng, fragment_mean, fragment_sd, length * 2, max_fragment)
template = sample_genomic_template(rng, eligible, fragment_length)
fragment = template.sequence
read1, s1, i1, d1 = introduce_errors(fragment[:length], rng, technology)
read2, s2, i2, d2 = introduce_errors(reverse_complement(fragment[-length:]), rng, technology)
read_id = f'{sample}:{number:08d}'
write_fastq_record(r1, f'{read_id}/1', read1, technology.quality_char * len(read1))
write_fastq_record(r2, f'{read_id}/2', read2, technology.quality_char * len(read2))
truth_rows.append(
[
read_id,
template.source_id,
template.start,
template.end,
template.strand,
fragment_length,
len(read1) + len(read2),
s1 + s2,
i1 + i2,
d1 + d2,
]
)
else:
if technology_name == 'illumina-se':
mean = read_length or technology.mean_length
sd = 0
minimum_length = mean
else:
mean = long_read_mean or read_length or technology.mean_length
sd = long_read_sd if long_read_sd is not None else technology.length_sd
minimum_length = 100
max_length = max(len(seq) for _, seq in simulation_sequences)
template_length = sample_positive_normal(rng, mean, sd, minimum_length, max_length)
template = sample_genomic_template(rng, simulation_sequences, template_length)
observed, substitutions, insertions, deletions = introduce_errors(template.sequence, rng, technology)
read_id = f'{sample}:{number:08d}'
write_fastq_record(
single,
read_id,
observed,
technology.quality_char * len(observed),
)
truth_rows.append(
[
read_id,
template.source_id,
template.start,
template.end,
template.strand,
template_length,
len(observed),
substitutions,
insertions,
deletions,
]
)
write_tsv(
truth_path,
[
'read_id',
'contig',
'start_0based',
'end_0based_exclusive',
'strand',
'template_length',
'observed_bases',
'substitutions',
'insertions',
'deletions',
],
truth_rows,
)
if variants:
variant_path = output_dir / 'truth' / 'variants.vcf'
write_rows(
variant_path,
[
['##fileformat=VCFv4.3'],
['##source=bioflow-sim'],
['#CHROM', 'POS', 'ID', 'REF', 'ALT', 'QUAL', 'FILTER', 'INFO'],
*[
[
variant.contig,
variant.position + 1,
f'SNV{index:06d}',
variant.reference,
variant.alternate,
'.',
'PASS',
'SIMULATED=1',
]
for index, variant in enumerate(variants, 1)
],
],
)
metadata: dict[str, object] = {
'schema_version': 1,
'assay': 'dna-sequencing',
'sample': sample,
'technology': technology_name,
'reference': str(reference.resolve()),
'seed': seed,
'read_units': reads,
'read_unit_definition': 'read pairs' if technology.paired else 'reads',
'output_files': output_files,
'truth_file': str(truth_path.relative_to(output_dir)),
'variant_truth': 'truth/variants.vcf' if variants else None,
'coordinate_system': '0-based half-open',
'parameters': {
'read_length': read_length,
'fragment_mean': fragment_mean,
'fragment_sd': fragment_sd,
'long_read_mean': long_read_mean,
'long_read_sd': long_read_sd,
'snvs': snvs,
},
}
write_json(output_dir / 'manifest.json', metadata)
logger.success('Generated {} {} in {}', reads, metadata['read_unit_definition'], output_dir)
return metadata

Xet Storage Details

Size:
7.77 kB
·
Xet hash:
5f43865a0afb11fc2d97486e5e9789910afcabaadd376ebd5a7b9207ddb76b9e

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.