-
-
Notifications
You must be signed in to change notification settings - Fork 1
Sleep Synthesizer #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # | ||
| # SPDX-FileCopyrightText: 2026 Stanford University, ETH Zurich, and the project authors (see CONTRIBUTORS.md) | ||
| # SPDX-FileCopyrightText: 2026 This source file is part of the SensorTSLM open-source project. | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| from __future__ import annotations | ||
|
|
||
| from extractors import CaptionExtractor, ChannelConfig | ||
| from synthesizers import CrossChannelSynthesizer | ||
| from timef.schema import Annotation, Recording | ||
|
|
||
|
|
||
| class CrossChannelExtractor(CaptionExtractor): | ||
| caption_type = "cross_channel" | ||
|
|
||
| def __init__(self, config: ChannelConfig, synthesizers: list[CrossChannelSynthesizer]): | ||
| super().__init__(config) | ||
| self.synthesizers = synthesizers | ||
|
|
||
| def extract(self, row: Recording) -> list[Annotation]: | ||
| results: list[Annotation] = [] | ||
| for synth in self.synthesizers: | ||
| results.extend(synth.synthesize(row, self.config)) | ||
| return results |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # | ||
| # SPDX-FileCopyrightText: 2026 Stanford University, ETH Zurich, and the project authors (see CONTRIBUTORS.md) | ||
| # SPDX-FileCopyrightText: 2026 This source file is part of the SensorTSLM open-source project. | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| from __future__ import annotations | ||
|
|
||
| import abc | ||
|
|
||
| import numpy as np | ||
|
|
||
| from extractors import ChannelConfig | ||
| from timef.schema import Annotation, Recording | ||
|
|
||
|
|
||
| class CrossChannelSynthesizer(abc.ABC): | ||
| @abc.abstractmethod | ||
| def synthesize(self, row: Recording, config: ChannelConfig) -> list[Annotation]: ... | ||
|
|
||
|
|
||
| def contiguous_windows(mask: np.ndarray, min_duration: int) -> list[tuple[int, int]]: | ||
| if not mask.any(): | ||
| return [] | ||
|
|
||
| padded = np.concatenate(([False], mask, [False])) | ||
| diffs = np.diff(padded.astype(np.int8)) | ||
| starts = np.where(diffs == 1)[0] | ||
| ends = np.where(diffs == -1)[0] | ||
| keep = (ends - starts) >= min_duration | ||
| return list(zip(starts[keep].tolist(), ends[keep].tolist())) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # | ||
| # SPDX-FileCopyrightText: 2026 Stanford University, ETH Zurich, and the project authors (see CONTRIBUTORS.md) | ||
| # SPDX-FileCopyrightText: 2026 This source file is part of the SensorTSLM open-source project. | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
|
|
||
| import numpy as np | ||
|
|
||
| from extractors import CaptionExtractor, ChannelConfig | ||
| from synthesizers import CrossChannelSynthesizer, contiguous_windows | ||
| from timef.schema import Annotation, Recording | ||
|
|
||
|
|
||
| class SleepSynthesizer(CrossChannelSynthesizer): | ||
| def __init__(self, min_duration: int = 5): | ||
| self.min_duration = min_duration | ||
|
|
||
| def synthesize(self, row: Recording, config: ChannelConfig) -> list[Annotation]: | ||
| try: | ||
| in_bed_idx = row.channel_names.index("sleep:inbed") | ||
| asleep_idx = row.channel_names.index("sleep:asleep") | ||
| except ValueError: | ||
| return [] | ||
|
|
||
| in_bed = np.asarray(row.values[in_bed_idx], dtype=float) | ||
| asleep = np.asarray(row.values[asleep_idx], dtype=float) | ||
| if not np.any((~np.isnan(asleep)) & (asleep > 0)): | ||
| return [] | ||
| mask = (~np.isnan(in_bed)) & (in_bed > 0) & ~((~np.isnan(asleep)) & (asleep > 0)) | ||
|
|
||
| time_unit = "hour" if config.time_unit == "hours" else "minute" | ||
| templates = json.loads(config.templates_path.read_text())["cross_channel"]["sleep"] | ||
| seed = CaptionExtractor._seed(row.row_id) | ||
|
|
||
| results: list[Annotation] = [] | ||
| for i, (start, end) in enumerate(contiguous_windows(mask, self.min_duration)): | ||
| end_inclusive = max(start, end - 1) | ||
| template = templates[(seed + i) % len(templates)] | ||
| text = template.format(time_unit=time_unit, start=start, end=end_inclusive) | ||
| results.append( | ||
| Annotation( | ||
| caption_type="cross_channel", | ||
| text=text, | ||
| channel_idxs=(asleep_idx, in_bed_idx), | ||
| window=(start, end), | ||
| label="in_bed_not_sleeping", | ||
| ) | ||
| ) | ||
| return results |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -114,13 +114,19 @@ def _nan_regions(arr: np.ndarray, min_length: int = 30) -> list[tuple[int, int]] | |
| from mhc.dataset import MHCDataset | ||
| from mhc.transformer import MHCTransformer | ||
| from mhc.constants import MHC_CHANNEL_CONFIG | ||
| from extractors.cross_channel import CrossChannelExtractor | ||
| from synthesizers.sleep import SleepSynthesizer | ||
| from extractors.statistical import StatisticalExtractor | ||
| from extractors.structural import StructuralExtractor | ||
| from annotator import Annotator | ||
| from captionizer import Captionizer | ||
|
|
||
| dataset = MHCDataset(min_wear_pct=90.0) | ||
| annotator = Annotator([StatisticalExtractor(MHC_CHANNEL_CONFIG), StructuralExtractor(MHC_CHANNEL_CONFIG)]) | ||
| annotator = Annotator([ | ||
| StatisticalExtractor(MHC_CHANNEL_CONFIG), | ||
| StructuralExtractor(MHC_CHANNEL_CONFIG), | ||
| CrossChannelExtractor(MHC_CHANNEL_CONFIG, synthesizers=[SleepSynthesizer()]), | ||
| ]) | ||
|
Comment on lines
+125
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wiring doesn't surface the new sleep annotations yet.
Possible follow-up in
|
||
| captionizer = Captionizer(dataset, MHCTransformer(), annotator) | ||
| result, _ = captionizer.run(max_rows=1) | ||
| for row in result.iter_rows(): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Help text references non-existent controls.
Line 757 mentions
target< / target>but these controls don't exist in the UI. The actual ways to change the jump target are:[or]keysConsider removing or correcting this line to avoid user confusion.
📝 Suggested fix
"up/down changes row", "left/right changes signal", - "target< / target> changes the jump target", + "click detector buttons to change the jump target", "n / p jump to next or previous hit for that target", "hit< / hit> buttons do the same", "[ / ] also changes the jump target",🤖 Prompt for AI Agents