Skip to content

Fix: Slow light schedule evaluation times#48

Open
ftavella wants to merge 11 commits into
Arcascope:mainfrom
ftavella:fix/issue-24
Open

Fix: Slow light schedule evaluation times#48
ftavella wants to merge 11 commits into
Arcascope:mainfrom
ftavella:fix/issue-24

Conversation

@ftavella
Copy link
Copy Markdown
Member

Summary

Fixes #24

  • Replace nested np.vectorize approach in __add__, __sub__, and concatenate_at with array-native function composition
  • Evaluation time for a sum of 24 pulses on 120,000 time points dropped from ~20+ seconds to ~1.5 ms

Changes

Root cause: __add__ wrapped lambda t: fn1(t) + fn2(t) in a new LightSchedule() call, which applied another np.vectorize on top. With n pulses this created n levels of vectorize nesting — each level iterating element-by-element in Python — giving O(n×L) scalar function calls for a time array of length L.

Fix:

  • LightSchedule.__init__ now stores _array_func: an array-native function that handles full numpy arrays efficiently. For from_pulse schedules this is the np.piecewise-based function directly; for constants it uses np.full_like; for arbitrary callables it detects array capability and falls back to np.vectorize only when needed.
  • __add__ and __sub__ combine _array_func references with a simple lambda (fn1(t) + fn2(t)) and bypass LightSchedule.__init__, avoiding any re-wrapping in np.vectorize. Evaluation is now O(n) array-level calls.
  • concatenate_at uses boolean-index slicing (result[mask] = fn(t[mask])) instead of np.piecewise with pre-evaluated arrays, making it array-native and correct.
  • _func (the np.vectorize-wrapped version) is kept on all objects for backward compatibility.

Tests

Added a new test in nbs/test/test_lights.ipynb:

  • Builds a schedule as the sum of 20 pulses over a 12-day, 120,000-point time array
  • Asserts output length is correct
  • Asserts evaluation completes in under 5 seconds (previously would take tens of seconds)
  • Verifies correct values at specific time points

Checklist

  • Changes made in notebooks under nbs/ (not .py files)
  • Tests added/updated in nbs/test/
  • nbdev_prepare runs successfully

ftavella and others added 11 commits March 11, 2026 10:31
Replace nested np.vectorize approach in __add__, __sub__, and
concatenate_at with array-native functions. Each LightSchedule now
stores an _array_func that operates on full numpy arrays efficiently.

- __init__: detect array-capable functions and store as _array_func;
  constants use np.full_like instead of scalar lambdas
- __add__ / __sub__: combine _array_func references without wrapping in
  a new np.vectorize call, cutting evaluation from O(n*L) to O(n+L)
- concatenate_at: use boolean-index slicing on arrays instead of
  np.piecewise with pre-computed arrays (which was incorrect for
  array inputs)
- _func kept as np.vectorize for backward compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests that LightSchedule.Chang14 (which uses both concatenate_at and
__add__ internally) evaluates 14 days of data at dt=0.005h in under
1 second for all six typical_indoor_lux values from the original
use-case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a LightSchedule is passed as a callable to another LightSchedule
(e.g. ShiftWork's LightSchedule(total_schedule, period=...)), calling
light(0.0) returns a 1-element array. In NumPy 1.25+ with
warnings-as-errors, float(1-element-array) raises DeprecationWarning
which was caught by the try/except and re-raised as a misleading
ValueError rejecting valid input.

Fix: use float(np.asarray(test_output).flat[0]) to safely extract a
scalar from any numeric type without triggering the deprecation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap _func scalar extraction so np.vectorize always receives a Python
float, even when the underlying callable (e.g. a nested LightSchedule)
returns a 1-element array.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LightSchedule.__call__ always returns a 1-d array; NumPy 2.4 strictly
rejects putting that array into a float slot in np.vectorize. All 4
`return schedule(time)` calls in ground_truth_shift_schedule now extract
a Python float explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ftavella ftavella self-assigned this Mar 11, 2026
@ftavella
Copy link
Copy Markdown
Member Author

Generating the light schedules of the evening light paper takes ~ 3 min with the old code:
image

and basically nothing with the new code:
image

and the files/schedules we are generating are exactly the same:

image image image

@ftavella ftavella requested a review from ojwalch March 11, 2026 17:49
@ftavella ftavella mentioned this pull request Mar 18, 2026
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.

Slow light schedule evaluation times

1 participant