codexcircadian-entrainment-phase-correction.py
circadian-entrainment-phase-correction.py
"""Illustrate a local, stipulated daily phase-correction map.
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
TAU = 23.5
ENVIRONMENT_PERIOD = 24.0
GAIN = 0.4
def delay(offset):
"""Positive delay, in hours; this is not a measured phase-response curve."""
return ENVIRONMENT_PERIOD - TAU - GAIN * offset
def phase_trace(initial=-3.0, cycles=10):
phase = [initial]
for _ in range(cycles):
phase.append(phase[-1] + TAU - ENVIRONMENT_PERIOD + delay(phase[-1]))
return np.array(phase)
def main():
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
"axes.spines.right": False})
fig, axes = plt.subplots(2, 1, figsize=(9, 6.4), dpi=100,
sharex=True, facecolor="white",
gridspec_kw={"height_ratios": [1.6, 1]})
fig.subplots_adjust(left=0.15, right=0.96, bottom=0.16, top=0.81, hspace=0.2)
n = np.arange(11)
phase = phase_trace()
uncoupled = -3.0 + (TAU - ENVIRONMENT_PERIOD) * n
axes[0].plot(n, phase, "o-", color="#087e8b", lw=2.2,
label="Daily phase correction")
axes[0].plot(n, uncoupled, "s--", color="#b94b32", lw=1.6,
label="No correction (free running)")
axes[0].axhline(0, color="#666666", ls=":", lw=1.4)
axes[0].set(ylabel="Onset offset xₙ (h)\nrelative to lights-off", ylim=(-8.6, 1.0))
axes[0].text(4.8, 0.45, "Stable phase relationship: x* = 0", fontsize=10)
axes[0].text(4.4, -4.0, "Negative offset = earlier onset", fontsize=10)
axes[0].legend(loc="lower left", frameon=False, fontsize=10)
axes[1].plot(n, delay(phase), "o-", color="#087e8b", lw=2.2)
axes[1].axhline(0.5, color="#666666", ls=":", lw=1.4)
axes[1].text(4.0, 1.45, "0.5 h delay balances τ − T = −0.5 h", fontsize=10)
axes[1].set(ylabel="Imposed daily\ndelay D(xₙ) (h)", xlabel="Environmental cycle n",
ylim=(0.3, 1.85), xlim=(-0.2, 10.3))
axes[1].set_xticks(np.arange(0, 11))
for ax in axes:
ax.grid(axis="y", color="#eeeeee")
ax.set_axisbelow(True)
fig.suptitle("Entrainment balances drift and restores phase", y=0.97, fontsize=15)
fig.text(0.5, 0.91, "Stipulated local map: τ = 23.5 h, T = 24 h, D(x) = 0.5 h − 0.4x",
ha="center", fontsize=11)
fig.text(0.5, 0.86, "xₙ₊₁ = xₙ + (τ − T) + D(xₙ) = 0.6xₙ; start at x₀ = −3 h",
ha="center", fontsize=11)
fig.text(0.5, 0.04, "Original conceptual model; no fitted biological gain or recorded activity.",
ha="center", fontsize=10, color="#555555")
fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()