#########################################################################################
##
## ZERO CROSSING EVENTS
## (events/zerocrossing.py)
##
## Milan Rother 2024
##
#########################################################################################
# IMPORTS ===============================================================================
import numpy as np
from ._event import Event
from .. _constants import TOLERANCE
# EVENT MANAGER CLASS ===================================================================
[docs]
class ZeroCrossing(Event):
"""Subclass of base 'Event' that triggers if the event function crosses zero.
This is a bidirectional zero-crossing detector.
Monitors system state by evaluating an event function (func_evt) with scalar output and
testing for zero crossings (sign changes).
.. code-block::
func_evt(time) -> event?
If an event is detected, some action (func_act) is performed on the system state.
.. code-block::
func_evt(time) == 0 -> event -> func_act(time)
Parameters
----------
func_evt : callable
event function, where zeros are events
func_act : callable
action function for event resolution
tolerance : float
tolerance to check if detection is close to actual event
"""
[docs]
def detect(self, t):
"""Evaluate the event function and check for zero-crossings
Parameters
----------
t : float
evaluation time for detection
Returns
-------
detected : bool
was an event detected?
close : bool
are we close to the event?
ratio : float
interpolated event location as ratio of timestep
"""
#evaluate event function
result = self.func_evt(t)
#are we close to the actual event?
close = abs(result) <= self.tolerance
#unpack history
_result, _t = self._history
#no history -> no zero crossing
if _result is None:
return False, False, 1.0
#check for zero crossing (sign change)
is_event = np.sign(result * _result) < 0
#definitely no event detected -> quit early
if not is_event:
return False, False, 1.0
#linear interpolation to find event time ratio (secant crosses x-axis)
ratio = abs(_result) / np.clip(abs(_result - result), TOLERANCE, None)
return True, close, float(ratio)
[docs]
class ZeroCrossingUp(Event):
"""Modification of standard 'ZeroCrossing' event where events are only triggered
if the event function changes sign from negative to positive (up). Also called
unidirectional zero-crossing.
"""
[docs]
def detect(self, t):
"""Evaluate the event function and check for zero-crossings
Parameters
----------
t : float
evaluation time for detection
Returns
-------
detected : bool
was an event detected?
close : bool
are we close to the event?
ratio : float
interpolated event location as ratio of timestep
"""
#evaluate event function
result = self.func_evt(t)
#are we close to the actual event?
close = abs(result) <= self.tolerance
#unpack history
_result, _t = self._history
#no history -> no zero crossing
if _result is None:
return False, False, 1.0
#check for zero crossing (sign change)
is_event = np.sign(result * _result) < 0 and result > _result
#no event detected or wrong direction -> quit early
if not is_event or _result >= 0:
return False, False, 1.0
#linear interpolation to find event time ratio (secant crosses x-axis)
ratio = abs(_result) / np.clip(abs(_result - result), TOLERANCE, None)
return True, close, float(ratio)
[docs]
class ZeroCrossingDown(Event):
"""Modification of standard 'ZeroCrossing' event where events are only triggered
if the event function changes sign from positive to negative (down). Also called
unidirectional zero-crossing.
"""
[docs]
def detect(self, t):
"""Evaluate the event function and check for zero-crossings
Parameters
----------
t : float
evaluation time for detection
Returns
-------
detected : bool
was an event detected?
close : bool
are we close to the event?
ratio : float
interpolated event location as ratio of timestep
"""
#evaluate event function
result = self.func_evt(t)
#are we close to the actual event?
close = abs(result) <= self.tolerance
#unpack history
_result, _t = self._history
#no history -> no zero crossing
if _result is None:
return False, False, 1.0
#check for zero crossing (sign change)
is_event = np.sign(result * _result) < 0 and result < _result
#no event detected or wrong direction -> quit early
if not is_event or _result <= 0:
return False, False, 1.0
#linear interpolation to find event time ratio (secant crosses x-axis)
ratio = abs(_result) / np.clip(abs(_result - result), TOLERANCE, None)
return True, close, float(ratio)