Skip to content

fix(dmn): 修热度图整片饱和与冷段跳到下周 (0.9.1) - #10

Merged
JohnRichard4096 merged 3 commits into
mainfrom
fix/heatmap-saturation
Nov 6, 2026
Merged

JohnRichard4096 merged 3 commits into
mainfrom
fix/heatmap-saturation

Conversation

@AmriaLin

@AmriaLin AmriaLin commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

症状

  1. 触发一轮调度后,热度图整片变成暖色,几乎每格都顶到最亮。
  2. 排期日期跳得离谱:周五 22:20 排到 2026-11-13 02:59。
  3. 周一到周三整天空着,冷段却没排到那里去。

原因

全亮:颜色分母用的是累计计数峰值(raw_fine)。升级当天累计计数只有新版本之后的消息(图里显示「观测 1 条」),分母被压到 1,而衰减权重普遍在 1~3 之间,ratio 全部超过 1 被截断成最亮。

跳周:_pick_run 按「起点是否已过」排序,今天已经开始、但还没结束的冷段被排到最后;跨到下周的同类时段起点在未来,反而排前面。

漏掉周一二三:候选冷段一直以「今天是星期几」为基准展开,只算今天与次日两天的表。周五晚上看,今天的表里冷段早被周五到周日的活动挤光,剩下的候选只有同星期几的下周时段,周一到周三整天空窗根本不参与比较。

改动

  • peak = max(1.0, max(weights), max(counts)):分母取累计峰值与当前权重的较大者,保留「随时间整体变淡」,又不会因为计数没攒够就顶满
  • 冷段搜索改为逐天展开今天起的一周(_candidates),把每天的冷段换成绝对时间、丢掉已结束的,再按真实距离挑最近的一段(_nearest);分层仍按 fine → mid → coarse 优先
  • _refine 改成收绝对区间,删除只按今天算的 _pick_run
  • IdlePlanner.load 增加迁移:老快照没有 raw_fine / raw_coarse 时,用衰减值作为累计计数起点

验证

  • 按热力图的分布构造 12 天数据(周一二三近乎全空),现在 2026-11-06 22:20、penalty 43:修复前落在下周五,修复后落在 2026-11-09 03:00(周一凌晨,52.7 小时后);penalty 1 / 10 / 43 三档结果一致
  • 全亮复现:构造「权重 0.4/1.5、累计计数仅 1 条」的状态,修复后 84 格中 13 格偏暖,其余正常渐变
  • ruff check / ruff format --check / pyright 全部通过

Summary by Sourcery

Fix heatmap intensity scaling and idle-window selection across weekly scheduling boundaries.

Bug Fixes:

  • Prevent heatmaps from becoming uniformly saturated when cumulative observation counts are low or migrated.
  • Choose idle scheduling windows from the nearest available periods across the next seven days, avoiding skipped current-day windows and implausible jumps into the following week.
  • Preserve and migrate legacy planner snapshots that lack cumulative counts, while indicating estimated observations in the heatmap.

Enhancements:

  • Refine scheduling against absolute time intervals and retain tiered fine-to-coarse selection with relaxation and fallback behavior.

Build:

  • Bump the project version to 0.9.1.

两个问题都出在 0.9.0 的新口径上。

热度图全亮:颜色分母取的是累计计数峰值,升级当天累计计数还没攒起来
(只有新版本之后的消息),分母被压到 1,而衰减权重普遍在 1~3 之间,
ratio 全部超过 1 被截断,整张图变成一片暖色。分母改为取累计峰值与
当前权重的较大者,既保留随时间变淡,也不会因为计数没攒够就顶满。

冷段跳到下周:_pick_run 原来用"起点是否已过"排序,今天已经开始的冷段
被排到最后,跨到下周的同类时段反而排前面,于是排期直接从周五 22 点
跳到下周五凌晨。改为看段尾是否还没结束,未结束的段交给 _refine
从当前时刻起算。

顺带补一个升级迁移:老快照没有 raw_fine/raw_coarse 时,用衰减值作为
累计计数起点,避免格内数字从 0 重来、和颜色对不上。
@sourcery-ai

sourcery-ai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

本次修复针对热度图升级后整片饱和和调度跨周跳跃两个问题:使用当前权重参与归一化、为旧快照迁移累计计数,并按冷段结束时间选择仍可用的时段;项目版本同步升级至 0.9.1。

Flow diagram for heatmap normalization and snapshot migration

flowchart TD
    A[Load planner snapshot] --> B{raw_fine or raw_coarse present?}
    B -->|Yes| C[Use cumulative counts]
    B -->|No| D[Use decayed fine/coarse values as baseline]
    C --> E[Compute weights with decay]
    D --> E
    E --> F["peak = max(1, max(weights), max(counts))"]
    F --> G[Render heatmap with gradual intensity]
Loading

Flow diagram for selecting an available cold scheduling segment

flowchart TD
    A[Candidate cold segments] --> B[Calculate segment start and end]
    B --> C{seg_end > now?}
    C -->|Yes| D[Mark segment as live]
    C -->|No| E[Mark segment as expired]
    D --> F[Rank live segments by delay and length]
    E --> F
    F --> G[Select best segment]
    G --> H[_refine starts from current time when needed]
Loading

File-Level Changes

Change Details Files
调整热度图归一化分母,避免升级或累计计数不足时整图饱和,同时保留随时间衰减的视觉趋势。
  • 将峰值分母改为累计计数峰值、当前衰减权重峰值和 1 的最大值。
amrita_plugin_memory/rethinking/heatmap.py
修正冷段选择逻辑,使当前尚未结束的时段优先于下周同类时段。
  • 按段尾是否晚于当前时间判断可用性,而不是按段起点是否已过去判断。
  • 保留最近开始时间和时段长度作为后续排序条件,并让 _refine 从当前时刻继续计算。
amrita_plugin_memory/rethinking/schedule.py
为缺少累计计数的旧快照增加迁移初始化,避免升级后数字从零开始且与颜色不一致。
  • 当 raw_fine 或 raw_coarse 为空时,分别用对应的衰减值初始化。
amrita_plugin_memory/rethinking/schedule.py
将项目版本提升至 0.9.1 并同步锁文件版本元数据。
  • 更新项目版本号及 uv.lock 中对应版本。
pyproject.toml
uv.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

周五晚上看周一整天空着,排期却跳到下周五凌晨:候选冷段一直以"今天
是星期几"为基准展开,周一到周三根本不在候选里。今天的表里冷段被
周五到周日的活动挤光后,只剩同星期几的下周时段可选。

改为逐天展开今天起的一周,把每天的冷段换成绝对时间、丢掉已结束的,
再按真实距离挑最近的一段;分层仍按 fine → mid → coarse 优先。
_refine 相应改成收绝对区间,原来的 _pick_run 删除。

复现:按热力图的分布构造 12 天数据(周一二三近乎全空),
现在 2026-11-06 22:20、penalty 43,修复前落在下周五,修复后落在
2026-11-09 03:00(周一凌晨,52.7 小时后)。

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="amrita_plugin_memory/rethinking/schedule.py" line_range="682" />
<code_context>
-            delay = max(0.0, (seg_start - now.astimezone(self.tz)).total_seconds())
-            key = (0 if delay > 0 else 1, delay, -length_slots)
+            seg_start, seg_end = self._seg_bounds(start_slot, length_slots, owner, now)
+            live = 0 if seg_end > now_local else 1
+            delay = max(0.0, (seg_start - now_local).total_seconds())
+            key = (live, delay, -length_slots)
</code_context>
<issue_to_address>
**Usable cold windows get skipped**

When the selected cold segment has too little time remaining for the run or safety buffer, while a later run in the same tier can fit, `_pick_run` ranks the in-progress segment first, and `_plan` refines only that selected run per tier. When it cannot fit, planning advances to another tier instead of trying the later run, potentially skipping a usable window or scheduling an unusable fallback.

Have `_plan` try the next run in the same tier when the selected run cannot produce a usable plan.
</issue_to_address>

### Comment 2
<location path="amrita_plugin_memory/rethinking/schedule.py" line_range="294-297" />
<code_context>
                 self.state.raw_coarse = raw_coarse
+            # 升级前的快照没有累计计数:拿衰减值当起点,
+            # 否则格内数字从 0 重来,和颜色对不上
+            if not any(self.state.raw_fine) and any(self.state.fine):
+                self.state.raw_fine = list(self.state.fine)
+            if not any(self.state.raw_coarse) and any(self.state.coarse):
+                self.state.raw_coarse = list(self.state.coarse)
             seen = raw.get("seen_days", [])
             if isinstance(seen, list):
</code_context>
<issue_to_address>
**Migrated counts are not raw observations**

When a saved state from before cumulative counters were stored is loaded, `IdlePlanner.load` copies decayed `fine` and `coarse` weights into the cumulative count fields for legacy snapshots. The heatmap and README then present those values as raw observation counts, so users upgrading from such snapshots see approximate decayed weights labeled as actual counts.

Clarify that counts in migrated snapshots are approximate, or preserve separate provenance so the UI can label them accurately.
</issue_to_address>

### Comment 3
<location path="amrita_plugin_memory/rethinking/heatmap.py" line_range="296" />
<code_context>
-    peak = max(1.0, max(counts)) if counts else 1.0
+    # 分母取累计峰值与当前峰值的较大者:既随时间整体变淡,也不会因为
+    # 累计计数还没攒起来就把每格都顶到最亮(升级当天的典型情况)
+    peak = max(1.0, max(weights), max(counts)) if counts else 1.0

     for wd in range(DAYS):
</code_context>
<issue_to_address>
**Heatmap stops fading**

When cumulative counts are still lower than the decayed fine-weight peak, as in the described upgrade-day state, `peak` follows the same decay as `weights` while the decayed maximum exceeds both 1 and the cumulative-count maximum, so the hottest cell stays at ratio 1 and the other cell ratios stay effectively unchanged. After an upgrade with low cumulative counts, the chart therefore does not fade overall until the weights drop below that fixed floor.

Normalize decayed weights against a stable peak reference, such as the corresponding pre-decay peak, rather than a maximum that decays in lockstep with the cells.
</issue_to_address>

### Comment 4
<location path="amrita_plugin_memory/rethinking/schedule.py" line_range="294-295" />
<code_context>
                 self.state.raw_coarse = raw_coarse
+            # 升级前的快照没有累计计数:拿衰减值当起点,
+            # 否则格内数字从 0 重来,和颜色对不上
+            if not any(self.state.raw_fine) and any(self.state.fine):
+                self.state.raw_fine = list(self.state.fine)
+            if not any(self.state.raw_coarse) and any(self.state.coarse):
+                self.state.raw_coarse = list(self.state.coarse)
</code_context>
<issue_to_address>
**Legacy activity is missing from counts**

When a legacy heatmap has older activity in cells with no post-upgrade observations, alongside at least one post-upgrade raw observation, `IdlePlanner.load` skips migration for the whole array as soon as any raw counter is nonzero. `render_heatmap_png` then uses raw counts for every cell, so legacy activity in cells without post-upgrade observations remains missing from displayed counts.

Migrate legacy weights into raw counts per cell whenever that cell has no raw observations, rather than gating migration on the whole array.

Also at `amrita_plugin_memory/rethinking/schedule.py:296-297`.
</issue_to_address>

### Comment 5
<location path="amrita_plugin_memory/rethinking/heatmap.py" line_range="294-295" />
<code_context>
     weights = [max(0.0, v) * decay for v in state.fine]
-    # 分母取累计峰值:整张图会随时间一起变淡,而不是每格重新拉满到最亮
-    peak = max(1.0, max(counts)) if counts else 1.0
+    # 分母取累计峰值与当前峰值的较大者:既随时间整体变淡,也不会因为
+    # 累计计数还没攒起来就把每格都顶到最亮(升级当天的典型情况)
+    peak = max(1.0, max(weights), max(counts)) if counts else 1.0

</code_context>
<issue_to_address>
**Explanatory comments span multiple lines**

When an explanation needs more than one line, the new consecutive `#` lines violate the single-line short-comment convention.

Rewrite the explanation as a concise single-line comment.

Also at `amrita_plugin_memory/rethinking/schedule.py:292-293`, `amrita_plugin_memory/rethinking/schedule.py:671-676`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 4 findings to address first, and when loading an older snapshot, the change copies decayed fine/coarse values into the new raw counters, and those inferred counters can affect later heatmap values after a revert. The effect is bounded and can be cleared or corrected by resetting the affected state; the scheduling change otherwise alters runtime slot selection and is reversible.

Blocking findings: amrita_plugin_memory/rethinking/schedule.py:682, amrita_plugin_memory/rethinking/schedule.py:297, amrita_plugin_memory/rethinking/heatmap.py:296, amrita_plugin_memory/rethinking/schedule.py:295


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

- _plan 选中最近一段后若 _refine 放不下,继续试同层后面的候选,
  不再直接跳到下一层或退回保底窗口
- 迁移改成逐格补位:老快照里只有部分格子有新计数时,
  其余格子仍用衰减值补上,累计计数不再漏掉旧活动
- 新增 migrated_counts 标记,含估算时图下副标题注明「含迁移估算」
- 颜色分母回到累计峰值(不衰减),恢复"整张图随时间变淡";
  累计计数为 0 时才退回当前权重,避免除零把每格顶满
- 按仓库规范把多行内联注释全部收成单行(heatmap、schedule、runner)
@JohnRichard4096
JohnRichard4096 merged commit 86f7e42 into main Nov 6, 2026
3 checks passed
@JohnRichard4096
JohnRichard4096 deleted the fix/heatmap-saturation branch November 6, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants