Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Refactor unary function pattern
  • Loading branch information
discohead committed Jun 9, 2025
commit bc4e60bce28782360be0e4c50ef2589491f68c67
4 changes: 2 additions & 2 deletions docs/patterns/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ View source: [core.py](https://github.com/ideoforms/isobar/tree/master/isobar/pa
| [PConstant](core/pconstant.md) | Returns a fixed value. |
| [PRef](core/pref.md) | Contains a reference to another pattern, which can be replaced dynamically. |
| [PFunc](core/pfunc.md) | Returns the value generated by a function. |
| [PUnaryFunction](core/punaryfunction.md) | Discretize a unary function across a range, with optional rate/phase control and output scaling. |
| [PCallableUnaryFunction](core/pcallableunaryfunction.md) | Like ``PUnaryFunction`` but the start/stop and modifiers are callables evaluated per-step. |
| [PUnaryFunction](core/punaryfunction.md) | Discretize a unary function across a range. Parameters may be constants, patterns, or callables. |
| [PCallableUnaryFunction](core/pcallableunaryfunction.md) | Enforces callable arguments for dynamic per-step control. |
| [PArrayIndex](core/parrayindex.md) | Request a specified index from an array. |
| [PDict](core/pdict.md) | Construct a pattern from a dict of arrays, or an array of dicts. |
| [PDictKey](core/pdictkey.md) | Request a specified key from a dictionary. |
Expand Down
129 changes: 73 additions & 56 deletions isobar/pattern/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,27 @@ class PUnaryFunction(Pattern):

def __init__(self,
function: Callable[[float], float],
start: float = 0.0,
stop: float = 1.0,
start: Callable[[float], float] | float | Pattern = 0.0,
stop: Callable[[float], float] | float | Pattern = 1.0,
steps: int = 100,
rate: float = 1.0,
phase: float = 0.0,
mul: float = 1.0,
offset: float = 0.0):
rate: Callable[[float], float] | float | Pattern = 1.0,
phase: Callable[[float], float] | float | Pattern = 0.0,
mul: Callable[[float], float] | float | Pattern = 1.0,
offset: Callable[[float], float] | float | Pattern = 0.0):
self.function = function
self.start = start
self._start_callable = callable(start)
self.stop = stop
self._stop_callable = callable(stop)
self.steps = steps
self.rate = rate
self._rate_callable = callable(rate)
self.phase = phase
self._phase_callable = callable(phase)
self.mul = mul
self._mul_callable = callable(mul)
self.offset = offset
self._offset_callable = callable(offset)
self.reset()

def __repr__(self):
Expand All @@ -52,24 +58,56 @@ def __next__(self):
steps = Pattern.value(self.steps)
if self.index >= steps:
raise StopIteration
start = Pattern.value(self.start)
stop = Pattern.value(self.stop)
fn = self.function

if steps <= 1:
t = 0.0
else:
t = float(self.index) / float(steps - 1)

if self._start_callable:
start = self.start(t)
else:
start = Pattern.value(self.start)

if self._stop_callable:
stop = self.stop(t)
else:
stop = Pattern.value(self.stop)

if steps <= 1:
x = start
else:
x = start + (stop - start) * float(self.index) / float(steps - 1)
rate = Pattern.value(self.rate)
phase = Pattern.value(self.phase)
x = start + (stop - start) * t

if self._rate_callable:
rate = self.rate(t)
else:
rate = Pattern.value(self.rate)

if self._phase_callable:
phase = self.phase(t)
else:
phase = Pattern.value(self.phase)

x = x * rate + phase

self.index += 1
value = fn(x)
mul = Pattern.value(self.mul)
offset = Pattern.value(self.offset)
value = self.function(x)

if self._mul_callable:
mul = self.mul(t)
else:
mul = Pattern.value(self.mul)

if self._offset_callable:
offset = self.offset(t)
else:
offset = Pattern.value(self.offset)

return value * mul + offset


class PCallableUnaryFunction(Pattern):
class PCallableUnaryFunction(PUnaryFunction):
"""Sample a unary function where parameters are callables.

Args:
Expand All @@ -94,46 +132,25 @@ def __init__(self,
phase: Callable[[float], float] = lambda t: 0.0,
mul: Callable[[float], float] = lambda t: 1.0,
offset: Callable[[float], float] = lambda t: 0.0):
self.function = function
self.start = start
self.stop = stop
self.steps = steps
self.rate = rate
self.phase = phase
self.mul = mul
self.offset = offset
self.reset()
for name, value in {
'start': start,
'stop': stop,
'rate': rate,
'phase': phase,
'mul': mul,
'offset': offset,
}.items():
if not callable(value):
raise TypeError(f"{name} must be callable")

def __repr__(self):
return (
f"PCallableUnaryFunction({self.function}, {self.start}, {self.stop}, "
f"{self.steps}, rate={self.rate}, phase={self.phase}, "
f"mul={self.mul}, offset={self.offset})"
super().__init__(
function,
start=start,
stop=stop,
steps=steps,
rate=rate,
phase=phase,
mul=mul,
offset=offset,
)

def reset(self):
super().reset()
self.index = 0

def __next__(self):
steps = Pattern.value(self.steps)
if self.index >= steps:
raise StopIteration
if steps <= 1:
t = 0.0
else:
t = float(self.index) / float(steps - 1)
start = self.start(t)
stop = self.stop(t)
if steps <= 1:
x = start
else:
x = start + (stop - start) * t
rate = self.rate(t)
phase = self.phase(t)
x = x * rate + phase
self.index += 1
value = self.function(x)
mul = self.mul(t)
offset = self.offset(t)
return value * mul + offset
13 changes: 13 additions & 0 deletions tests/test_pattern_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ def test_punaryfunction_rate_phase():
assert a.nextn(3) == [0.25, 2.25, 6.25]


def test_punaryfunction_callable_mix():
f = lambda x: x * x
p = iso.PUnaryFunction(
f,
start=0.0,
stop=1.0,
steps=3,
mul=lambda t: 2.0,
offset=lambda t: t,
)
assert p.nextn(3) == [0.0, 1.0, 3.0]


def test_pcallableunaryfunction_constant():
f = lambda x: x * x
c = iso.PCallableUnaryFunction(
Expand Down