Skip to content

Add function for exploding intervals to loci - #789

Open
Ruchit10 wants to merge 48 commits into
broadinstitute:mainfrom
Ruchit10:rp/add_explode_loci_from_interval
Open

Add function for exploding intervals to loci#789
Ruchit10 wants to merge 48 commits into
broadinstitute:mainfrom
Ruchit10:rp/add_explode_loci_from_interval

Conversation

@Ruchit10

@Ruchit10 Ruchit10 commented Jul 3, 2025

Copy link
Copy Markdown

Feature Addition:

Added a function in intervals.py which explodes hail intervals into per base loci, keeping same reference genome as original interval.

Optionally it can return the original interval that the locus belongs to or drop it from the exploded table.

@Ruchit10
Ruchit10 requested a review from a team as a code owner July 3, 2025 19:18

@ch-kr ch-kr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding! a few suggestions

Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py
Ruchit10 and others added 2 commits July 3, 2025 17:09
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
@Ruchit10
Ruchit10 requested a review from ch-kr July 10, 2025 19:47

@ch-kr ch-kr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few more minor changes -- sorry about the back and forth on the MT part!

Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Ruchit10 and others added 13 commits July 11, 2025 17:14
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
@Ruchit10
Ruchit10 requested a review from ch-kr July 14, 2025 17:22

@ch-kr ch-kr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for adding additional flexibility! I've added some suggestions to streamline the code (and hopefully didn't break the formatting too much)

Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Ruchit10 and others added 5 commits July 15, 2025 11:13
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Ruchit10 and others added 3 commits July 15, 2025 11:18
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
Co-authored-by: Katherine Chao <kchao@broadinstitute.org>
@ch-kr

ch-kr commented Sep 16, 2025

Copy link
Copy Markdown
Contributor

a little late, but I also forgot that we've been requesting that each person who adds or changes code in this repo add tests (https://github.com/broadinstitute/gnomad_methods/tree/main/tests). could you add tests for this new function?

@Ruchit10
Ruchit10 requested a review from ch-kr April 24, 2026 19:15

@ch-kr ch-kr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding tests! a few more thoughts

Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py
Comment thread tests/utils/test_interval_utils.py Outdated
@pytest.fixture
def sample_interval_expr(self):
"""Fixture to create a sample interval expression."""
return hl.literal(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need this literal? hl.interval returns an IntervalExpression

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is very silly on my end. I was using hl.Interval instead of hl.interval before so was wondering why the interval function wasn't working (which is reason for adding hl.literal). Will change this

Comment thread tests/utils/test_interval_utils.py Outdated
Comment thread tests/utils/test_interval_utils.py Outdated
Comment thread tests/utils/test_interval_utils.py Outdated
Comment thread tests/utils/test_interval_utils.py

@ch-kr ch-kr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few more comments

Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py
Comment thread tests/utils/test_interval_utils.py
Comment thread tests/utils/test_interval_utils.py
Comment thread tests/utils/test_interval_utils.py
Comment thread tests/utils/test_interval_utils.py Outdated

@ch-kr ch-kr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few more style comments

Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment on lines +209 to +210
"Input is a list of IntervalExpressions, so function will return an"
" ArrayExpression of loci within all input intervals."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should these be consistent types? the current behavior means the user passes in a list but receives an ArrayExpression

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel an ArrayExpression as input makes more sense in that case. That will be the most common input type in hail isn't it?

Comment thread gnomad/utils/intervals.py Outdated
Comment thread gnomad/utils/intervals.py Outdated
Comment on lines +275 to +281
return interval_to_pos_range(intervals_expr).map(
lambda pos: hl.locus(
intervals_expr.start.contig,
pos,
reference_genome=intervals_expr.start.dtype.reference_genome,
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be replaced with a call to _make_loci_array

Comment thread gnomad/utils/intervals.py Outdated
)

loci_arrays = [_make_loci_array(i) for i in intervals]
if not flatten:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consistency, this probably should be reordered to be if flatten:

Comment thread gnomad/utils/intervals.py Outdated
Comment on lines +244 to +250
intervals = intervals.key_by(
locus=hl.locus(
intervals[interval_field].start.contig,
intervals._pos,
reference_genome=get_reference_genome(intervals[interval_field]),
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can also be replaced by a call to _make_loci_array

Comment thread gnomad/utils/intervals.py
" intervals overlap."
)
else:
intervals = intervals.distinct()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a warning here that distinct() arbitrarily deduplicates, which means that if the input table was annotated with something like gene or transcript ID, this will pick one at random for duplicated loci?

Comment thread gnomad/utils/intervals.py Outdated
Comment on lines +228 to +233
if not flatten:
return hl.array(loci_arrays)
result = hl.flatten(hl.array(loci_arrays))
if deduplicate:
result = hl.array(hl.set(result))
return result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be a check to make sure the positions are sorted?

Comment thread gnomad/utils/intervals.py
result = hl.array(hl.set(result))
return result

intervals_expr = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like this expression only gets used once (it doesn't get used if this is a Table below); is this code necessary?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also gets used in a table to annotate _pos, but it gets wrapped into interval_to_pos_range either way so maybe I should make that a single call upstream

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what I meant when I wrote this comment was that you could directly call _make_loci_array on intervals[interval_field], since the only case that is handled below is if isinstance(intervals, hl.Table). however, if you reorder the code as suggested above, you shouldn't need this if/else at all

@ch-kr ch-kr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few more comments after revisiting

Comment thread gnomad/utils/intervals.py
Comment on lines +165 to +166
.. warning::
- Overlapping intervals will produce duplicate loci. Use ``deduplicate=True``

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. warning::
- Overlapping intervals will produce duplicate loci. Use ``deduplicate=True``
.. warning::
- Overlapping intervals will produce duplicate loci. Use ``deduplicate=True``

nit

Comment thread gnomad/utils/intervals.py
chromosomes), as it will create extremely large arrays, which may cause
performance issues.

Note that intervals that cross chromosomes are currently not supported.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should raise an error if a user passes an interval that crosses chromosomes?

Comment thread gnomad/utils/intervals.py
" IntervalExpressions!"
)

if isinstance(intervals, hl.Table) and (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seeing this function again with fresh eyes, you could reorder how you tackle the different inputs (Table, IntervalExpression, ArrayExpression). the current function structure checks whether the input is a Table 3 separate times, and reordering would remove the extra checks.

by reordering, I mean this function could handle the case of an input IntervalExpression + return, then the case of an input ArrayExpression + return, and finally the case of an input Table

Comment thread gnomad/utils/intervals.py
result = hl.array(hl.set(result))
return result

intervals_expr = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what I meant when I wrote this comment was that you could directly call _make_loci_array on intervals[interval_field], since the only case that is handled below is if isinstance(intervals, hl.Table). however, if you reorder the code as suggested above, you shouldn't need this if/else at all

Comment thread gnomad/utils/intervals.py
interval_field: Optional[str] = None,
keep_intervals: Optional[bool] = False,
deduplicate: bool = True,
flatten: bool = True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there doesn't seem to be a test covering flatten=False in the test suite

Comment thread gnomad/utils/intervals.py
result = hl.flatten(loci_arrays)
if deduplicate:
result = hl.array(hl.set(result))
result = hl.sorted(result)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add a test checking that results are sorted when flatten is True

Comment thread gnomad/utils/intervals.py

intervals = intervals.drop(*fields_to_drop)

if deduplicate:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the other logic branch (deduplicate=False, keep_intervals=True) case also be covered in tests?

Comment on lines +169 to +174
interval = hl.interval(
hl.locus("chr1", 100, "GRCh38"),
hl.locus("chr1", 105, "GRCh38"),
includes_start=False,
includes_end=False,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like the same interval that is built into the function above. could this be extracted into a fixture?

Comment on lines +348 to +351
ht = hl.Table.parallelize(
[{"interval": interval, "gene": "GENE1"}],
hl.tstruct(interval=hl.tinterval(hl.tlocus("GRCh38")), gene=hl.tstr),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are a few hl.Table.parallelizes in the tests as well, could you update the fixture or create other fixtures to reduce redundancy?

# Verify that other fields are still present.
assert all(hasattr(row, "gene") for row in result)

def test_explode_interval_expression(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the multiple test_explode_interval_expression* tests that only change testing whether the start/end are included/excluded can be collapsed into a single@pytest.mark.parametrized function that uses a table with a few rows that vary the start/end inclusion. this is also true of the test_explode_table_*excludes* tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants