Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Fix typo in `ar_model.py` that causes `AttributeError` during evaluation [\#204](https://github.com/mllam/neural-lam/pull/204) @ritinikhil

- Fix `get_integer_time` to avoid floating-point precision issues and correctly handle zero timedelta [#494](https://github.com/mllam/neural-lam/pull/494) @Saptami191

- Changed the hardcoded True to a conditional check "persistent_workers=self.num_workers > 0" [\#235](https://github.com/mllam/neural-lam/pull/235) @santhil-cyber

- Avoid eager download of the MEPS example dataset during pytest collection by lazily initializing it in `tests/conftest.py`, allowing tests to run without triggering a dataset download at import time. [#391](https://github.com/mllam/neural-lam/pull/391) @Saptami191
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def get_original_indices(self):
return self.original_indices

def get_original_window_indices(self, step_length):
step_int, _ = get_integer_time(step_length.total_seconds())
step_int, _ = get_integer_time(step_length)
return [
i // step_int for i in range(len(self.original_indices) * step_int)
]
Expand Down
33 changes: 21 additions & 12 deletions neural_lam/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,22 +601,31 @@ def get_integer_time(tdelta) -> tuple[int, str]:
>>> get_integer_time(timedelta(milliseconds=1000))
(1, 'seconds')
>>> get_integer_time(timedelta(days=0.001))
(1, 'unknown')
(86400, 'milliseconds')
>>> get_integer_time(timedelta(0))
(0, 'seconds')
"""
total_seconds = tdelta.total_seconds()
total_microseconds = (
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 new implementation is fine, but this helper’s contract is still inconsistent across the tree: compute_standardization_stats.py still calls get_integer_time(step_length.total_seconds()), i.e. with a float rather than a timedelta. I think we should either update that caller in the same slice or explicitly tighten/document the contract here.

tdelta.days * 86400_000000
+ tdelta.seconds * 1_000000
+ tdelta.microseconds
)

if total_microseconds == 0:
return 0, "seconds"

units = {
"weeks": 604800,
"days": 86400,
"hours": 3600,
"minutes": 60,
"seconds": 1,
"milliseconds": 0.001,
"microseconds": 0.000001,
"weeks": 604800_000000,
"days": 86400_000000,
"hours": 3600_000000,
"minutes": 60_000000,
"seconds": 1_000000,
"milliseconds": 1_000,
"microseconds": 1,
}

for unit, unit_in_seconds in units.items():
if total_seconds % unit_in_seconds == 0:
return int(total_seconds / unit_in_seconds), unit
for unit, unit_us in units.items():
if total_microseconds % unit_us == 0:
return total_microseconds // unit_us, unit

return 1, "unknown"
29 changes: 29 additions & 0 deletions tests/test_utils.py
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.

All these newly added tests are already covered by the doctest. No need to replicate them here. If you think a case is missing in the docstring, please add them there.

Copy link
Copy Markdown
Contributor Author

@Saptami191 Saptami191 Mar 23, 2026

Choose a reason for hiding this comment

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

@observingClouds thanks for the review! yeah all are tested in docstring. i will remove this test file .actually it was part of proving the fix is correct.
Should I also add a doctest for negative timedelta

get_integer_time(timedelta(days=-7))
(-1, 'weeks')

it will represent a duration in the past 7 days ago.
or is that out of scope for this PR?

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Standard library
from datetime import timedelta

# First-party
from neural_lam.utils import get_integer_time


def test_days():
assert get_integer_time(timedelta(days=14)) == (2, "weeks")


def test_hours():
assert get_integer_time(timedelta(hours=5)) == (5, "hours")


def test_zero():
assert get_integer_time(timedelta(0)) == (0, "seconds")


def test_milliseconds():
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.

Since this PR is specifically about float-precision avoidance, I think it would be good to add one direct regression test for the issue-body example (timedelta(days=0.001) -> (86400, "milliseconds")) here as well, rather than leaving that behavior covered only by the doctest.

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 is actually already tested in the docstring.

assert get_integer_time(timedelta(milliseconds=1000)) == (1, "seconds")


def test_negative():
assert get_integer_time(timedelta(days=-7)) == (-1, "weeks")


def test_float_days():
assert get_integer_time(timedelta(days=0.001)) == (86400, "milliseconds")
Loading