Relaxation toward a profile, in whole steps¶
Relaxation toward a profile, in integer steps.
- Requires
- a value and its target, integers at the quantity's quantum; the fraction f of the gap closed per step (0..1).
- Provides
- the value after the step.
- Law
- v' = v + f (t - v), truncated toward zero, and at least one unit toward the target while there is a gap, so it arrives: Newton's law of cooling in its discrete form, the gap decaying as (1 - f)^n.
- Source
- Bergman et al., Fundamentals of Heat and Mass Transfer, 5.2 (lumped capacitance).
- Proof
- curve, invariant, limit.
No Trait form: waits on an unconstrained unit parameter, `<Q>` bound at `use`: the value relaxed is an amount of whatever the game has, a temperature or a stock, so the law is written once for any unit.
laws.relax(value, target, fraction)A value closes a fraction of its gap to a target each step, in integers, and arrives.
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import dmath as D, library
from lusine.kernels.laws.relax import relax
The reference¶
The kernel's reference, in numpy: the body under the declaration above, and what every section below runs. It stays as the kernel's test beside its Trait form.
library.reference(relax)
the reference, relax.py
def relax(value, target, fraction):
"""RELAXATION TOWARD A PROFILE, in integer steps.
Requires: a value and its target, integers at the quantity's quantum;
the fraction f of the gap closed per step (0..1).
Provides: the value after the step.
Law: v' = v + f (t - v), truncated toward zero, and at least one unit
toward the target while there is a gap, so it arrives: Newton's law of
cooling in its discrete form, the gap decaying as (1 - f)^n.
Source: Bergman et al., Fundamentals of Heat and Mass Transfer, 5.2
(lumped capacitance).
Proof: curve, invariant, limit."""
v = np.asarray(value, dtype=np.int64)
t = np.asarray(target, dtype=np.int64)
gap = t - v
step = np.trunc(gap.astype(np.float64) * float(fraction)).astype(np.int64)
step = np.where((step == 0) & (gap != 0), np.sign(gap), step)
return v + step
Curve: the gap falls as (1 - f)^n¶
From 0 toward a million at f = 0.1, the gap after n steps against (1 - f)^n.
v, t, f = 0, 1_000_000, 0.1
gaps = [t - v]
for _ in range(40):
v = int(relax(v, t, f))
gaps.append(t - v)
n = np.arange(41)
assert np.max(np.abs(np.array(gaps[:20]) / (t * (1 - f) ** n[:20]) - 1)) < 0.01
fig, ax = plt.subplots(figsize=(7, 3))
ax.semilogy(n, t * (1 - f) ** n, label='(1 - f)^n')
ax.semilogy(n, np.maximum(gaps, 0.5), 'o', ms=3, label='relax')
ax.legend()
ax.set_xlabel('step')
plt.show()
Invariant: never past the target¶
Relaxation is monotone: from below it rises and from above it falls, and it never overshoots, at any fraction.
for f in (0.05, 0.3, 0.9, 1.0):
for start, target in ((0, 997), (5000, -3)):
x = start
for _ in range(60):
y = int(relax(x, target, f))
assert (x <= y <= target) if start < target else (target <= y <= x)
x = y
print('monotone and bounded at every fraction')
monotone and bounded at every fraction
Limit: at the target it stays; f = 1 arrives at once; whole units arrive¶
The target is a fixed point, the whole gap closes in one step at f = 1, and a small gap closes a unit at a time rather than stalling below one (the law's floor of one unit while a gap remains).
assert int(relax(42, 42, 0.3)) == 42 and int(relax(10, 50, 1.0)) == 50 and int(relax(10, 50, 0.0)) == 11
x = 5
for _ in range(20):
x = int(relax(x, 7, 0.1))
assert x == 7
print('arrived at', x)
arrived at 7