codexcircadian-free-running-actogram.py
circadian-free-running-actogram.py
"""Draw a stipulated circadian actogram, not a record from an animal.
Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; the PNG is written to the current directory.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Patch, Rectangle
TAU = 23.5
ENVIRONMENT_PERIOD = 24.0
LD_DAYS = 7
TOTAL_DAYS = 24
ACTIVITY_DURATION = 8.0
RELEASE = LD_DAYS * ENVIRONMENT_PERIOD
def activity_intervals():
"""Absolute-time intervals; the first DD onset preserves the LD phase."""
intervals = [(24.0 * day + 12.0, 24.0 * day + 20.0)
for day in range(LD_DAYS)]
onset = RELEASE + 12.0
while onset < TOTAL_DAYS * ENVIRONMENT_PERIOD:
intervals.append((onset, onset + ACTIVITY_DURATION))
onset += TAU
return intervals
def main():
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
"axes.spines.right": False})
fig, ax = plt.subplots(figsize=(9, 6.6), dpi=100, facecolor="white")
fig.subplots_adjust(left=0.16, right=0.97, bottom=0.19, top=0.83)
for day in range(TOTAL_DAYS):
ax.add_patch(Rectangle((0, day + 0.5), 24, 1,
facecolor="#edf0f3", edgecolor="white", lw=0.8))
if day < LD_DAYS:
ax.add_patch(Rectangle((0, day + 0.5), 12, 1,
facecolor="#fff2c9", edgecolor="white", lw=0.8))
for start, end in activity_intervals():
for day in range(TOTAL_DAYS):
left = max(start, day * 24.0)
right = min(end, (day + 1) * 24.0)
if right > left:
ax.add_patch(Rectangle((left - day * 24.0, day + 0.68),
right - left, 0.64,
facecolor="#087e8b", edgecolor="none"))
day = int(start // 24.0)
if day < TOTAL_DAYS:
ax.plot(start % 24, day + 1, "|", color="#003e47", markersize=11)
ax.axhline(LD_DAYS + 0.5, color="#b94b32", lw=1.5, ls="--")
ax.text(-2.7, 4, "12:12 LD", rotation=90, ha="center", va="center",
color="#66521e", clip_on=False)
ax.text(-2.7, 16, "Constant darkness (DD)", rotation=90,
ha="center", va="center", color="#49535e", clip_on=False)
ax.annotate("First DD onset:\n12 h", xy=(12, 8), xytext=(20.5, 10),
fontsize=10, arrowprops={"arrowstyle": "->", "color": "#003e47"})
ax.annotate("17th DD onset: 4 h", xy=(4, 24), xytext=(17, 22),
fontsize=10, arrowprops={"arrowstyle": "->", "color": "#003e47"})
ax.set(xlim=(0, 24), ylim=(24.65, 0.4), xlabel="Hour within a fixed 24-hour plotting row",
ylabel="Successive calendar day")
ax.set_xticks(np.arange(0, 25, 4))
ax.set_yticks([1, 7, 8, 12, 16, 20, 24])
fig.suptitle("A clock that keeps running when the light cycle stops", y=0.97, fontsize=15)
fig.text(0.5, 0.915, "Original idealized model: τ = 23.5 h; every activity bout lasts 8 h",
ha="center", fontsize=11)
fig.legend(handles=[Patch(facecolor="#fff2c9", label="Light"),
Patch(facecolor="#edf0f3", label="Dark"),
Patch(facecolor="#087e8b", label="Constructed activity")],
loc="upper center", bbox_to_anchor=(0.5, 0.9), ncol=3, frameon=False)
fig.text(0.5, 0.065, "DD onsets are 23.5 h apart: 0.5 h earlier per cycle on this 24 h grid.",
ha="center", fontsize=11)
fig.text(0.5, 0.027, "Stipulated phase and activity windows; no mouse or human observations.",
ha="center", fontsize=10, color="#555555")
fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()