diff --git a/.gitignore b/.gitignore index a80bac4..cbbadcb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ __pycache__/ build/ .pytest_cache/ dist/ -*.egg-info/ \ No newline at end of file +*.egg-info/ +.pypirc \ No newline at end of file diff --git a/packages/sfm-analysis/README.md b/packages/sfm-analysis/README.md index 1da3892..8196efd 100644 --- a/packages/sfm-analysis/README.md +++ b/packages/sfm-analysis/README.md @@ -183,10 +183,7 @@ tool. Absent entirely for shorter runs. It deliberately draws **no light/dark shading**. The rig doesn't record the facility's light schedule, so any shading would be a fixed clock-time assumption rendered as though it were measured data. Time of -day is on the axis; apply your own light cycle to it. (For zeitgeber -time in your own analysis, `report.timezones.zeitgeber_time(row, -lights_on=...)` takes the schedule explicitly, where it's your stated -input rather than a silent report-wide default.) +day is on the axis; apply your own light cycle to it if you need one. The section heading and figure caption both name the plotted event (`Actogram — MousePresence Detected` by default) so a printed page is diff --git a/packages/sfm-analysis/docs/ANALYSIS_GUIDE.md b/packages/sfm-analysis/docs/ANALYSIS_GUIDE.md index 5baa77a..5b874a9 100644 --- a/packages/sfm-analysis/docs/ANALYSIS_GUIDE.md +++ b/packages/sfm-analysis/docs/ANALYSIS_GUIDE.md @@ -299,9 +299,7 @@ as does the figure's own SVG ``. The actogram draws no light/dark shading: the rig doesn't record the facility's light schedule, so shading it would render a fixed clock-time assumption as though it were measured data. Time of day is -on the axis — apply your own light cycle to it, or use -`zeitgeber_time(row, lights_on=...)` ([§7](#7-time-timezone-and-time-of-day)), -where the schedule is an explicit input you supply. +on the axis — apply your own light cycle to it if you need one. A pip-installed user does not edit the package: @@ -361,7 +359,7 @@ questions: | `timestamp_ms` | epoch milliseconds | Correct for ordering; needs `utc_offset_s` to mean anything as wall-clock | | `timestamp_iso` (`row.iso`) | rig-local wall-clock string | Already correct local time, with **no offset needed** — see below | -`report.timezones` gives you four functions, all reading `row.iso` +`report.timezones` gives you three functions, all reading `row.iso` directly rather than reconstructing from epoch ms — because `datetime.fromtimestamp()`/`.astimezone()` with no arguments implicitly use *your own machine's* timezone, which is wrong the moment analyst and rig @@ -370,10 +368,7 @@ are in different zones: - **`time_of_day(row)`** → hours since local midnight (`0.0`–`24.0`). No offset needed: `timestamp_iso` was already written in rig-local time. - **`local_date(row)`** → the calendar date this row falls on, rig-local. -- **`zeitgeber_time(row, lights_on=6.0)`** → hours relative to lights-on - (ZT0 = lights-on). Also needs no offset — your facility's light schedule - is itself set in local wall-clock time. -- **`wall_clock(row, run)`** → the *only* one of the four that needs +- **`wall_clock(row, run)`** → the *only* one of the three that needs `run.utc_offset_s`. Returns a timezone-**aware** `datetime` when the offset is known, a naive one otherwise — the naive case is not a failure, the value is still correct rig-local time, it's just unsafe to diff --git a/packages/sfm-analysis/src/sfm_analysis/analysis/__init__.py b/packages/sfm-analysis/src/sfm_analysis/analysis/__init__.py index bc28baf..c3188a3 100644 --- a/packages/sfm-analysis/src/sfm_analysis/analysis/__init__.py +++ b/packages/sfm-analysis/src/sfm_analysis/analysis/__init__.py @@ -24,7 +24,7 @@ from .intervals import around, count_in, merge, overlap, rate_in, subtract from .session import AmbiguousSessionError, Session, SessionNotFoundError, load_session from .tables import bouts_table, cycles_table, events_table, to_dataframe, trials_table -from ..report.timezones import local_date, time_of_day, wall_clock, zeitgeber_time +from ..report.timezones import local_date, time_of_day, wall_clock __all__ = [ "load_session", @@ -45,6 +45,5 @@ "rate_in", "time_of_day", "local_date", - "zeitgeber_time", "wall_clock", ] diff --git a/packages/sfm-analysis/src/sfm_analysis/report/timezones.py b/packages/sfm-analysis/src/sfm_analysis/report/timezones.py index d8d9f53..3d50626 100644 --- a/packages/sfm-analysis/src/sfm_analysis/report/timezones.py +++ b/packages/sfm-analysis/src/sfm_analysis/report/timezones.py @@ -55,21 +55,6 @@ def local_date(row: "LogRow"): return datetime.fromisoformat(row.iso).date() -def zeitgeber_time(row: "LogRow", *, lights_on: float = 6.0) -> float: - """ - Zeitgeber time in hours (``0.0`` to just under ``24.0``, wrapped), - relative to ``lights_on`` — ZT0 is lights-on, ZT12 is nominally - lights-off under a standard 12:12 light/dark cycle. - - ``lights_on`` is hours since local midnight (default ``6.0`` = - 06:00); pass your facility's actual light-cycle start time. Light - schedules are set in local wall-clock time by the people running the - facility, so — same reasoning as ``time_of_day`` — no UTC offset is - needed here either. - """ - return (time_of_day(row) - lights_on) % 24.0 - - def wall_clock(row: "LogRow", run: "RunData") -> datetime: """ A ``datetime`` for this row: timezone-*aware* if ``run.utc_offset_s`` diff --git a/packages/sfm-analysis/tests/test_report_timezones.py b/packages/sfm-analysis/tests/test_report_timezones.py index 015bae1..c68109b 100644 --- a/packages/sfm-analysis/tests/test_report_timezones.py +++ b/packages/sfm-analysis/tests/test_report_timezones.py @@ -7,7 +7,7 @@ from sfm_analysis.report.loader import load_rows from sfm_analysis.report.session import split_runs from sfm_analysis.report.timezones import ( - local_date, time_of_day, wall_clock, zeitgeber_time, + local_date, time_of_day, wall_clock, ) @@ -56,30 +56,6 @@ def test_matches_the_isos_own_date(self, tmp_path): assert local_date(lr) == datetime.fromisoformat(lr.iso).date() -class TestZeitgeberTime: - def test_zt0_at_lights_on(self, tmp_path): - rows = [row(ts_ms=1_700_000_000_000 + i * 3_600_000) for i in range(24)] - path = write_session(tmp_path, rows, session="S") - loaded, _, _ = load_rows(path) - run = split_runs(loaded, [], path)[0] - for lr in run.rows: - tod = time_of_day(lr) - zt = zeitgeber_time(lr, lights_on=tod) - assert zt == 0.0 - - def test_wraps_around_midnight(self): - # A row whose time_of_day is 1.0 and lights_on=23.0 should give - # zt=2.0 (1.0 - 23.0 = -22.0, wrapped mod 24 -> 2.0). - class _FakeRow: - iso = "2026-01-01T01:00:00" - assert zeitgeber_time(_FakeRow(), lights_on=23.0) == 2.0 - - def test_default_lights_on_is_6am(self): - class _FakeRow: - iso = "2026-01-01T06:00:00" - assert zeitgeber_time(_FakeRow()) == 0.0 - - class TestWallClock: def test_naive_when_utc_offset_unknown(self, tmp_path): path = write_session(tmp_path, [_row_at(1_700_000_000_000)], session="S")