aishafenton-hysteresis_filters 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.txt +55 -0
- data/Rakefile +14 -0
- data/lib/hysteresis_filters.rb +115 -0
- data/spec/motion_start_spec.rb +63 -0
- data/spec/schmitt_trigger_spec.rb +26 -0
- data/spec/speed_alert_spec.rb +81 -0
- data/spec/tolerance_trigger_spec.rb +22 -0
- data/spec/transition_trigger_spec.rb +28 -0
- metadata +60 -0
data/README.txt
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
HysteresisFilters
|
2
|
+
by VisFleet Ltd
|
3
|
+
http://www.visfleet.com
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A collection of simple decision filters which have the Hysteresis property. The filters provide
|
8
|
+
simple boolean logic but with a little added fuzzyness. The fuzzyness is based on what states
|
9
|
+
a filter has previously been through (i.e. Hysteresis). These filters are useful when you want
|
10
|
+
to: smooth away brief changes in state (e.g. Tolerance filter), make your logic robust against
|
11
|
+
flipping between states (e.g. Schmitt filter), or only want to know when something has
|
12
|
+
changed (e.g. Transition filter).
|
13
|
+
|
14
|
+
Each trigger can be used by itself or chained together to make surprisingly complex behaviours.
|
15
|
+
|
16
|
+
== FEATURES/PROBLEMS:
|
17
|
+
|
18
|
+
* FIX (list of features or problems)
|
19
|
+
|
20
|
+
== SYNOPSIS:
|
21
|
+
|
22
|
+
FIX (code sample of usage)
|
23
|
+
|
24
|
+
== REQUIREMENTS:
|
25
|
+
|
26
|
+
* FIX (list of requirements)
|
27
|
+
|
28
|
+
== INSTALL:
|
29
|
+
|
30
|
+
* FIX (sudo gem install, anything else)
|
31
|
+
|
32
|
+
== LICENSE:
|
33
|
+
|
34
|
+
(The MIT License)
|
35
|
+
|
36
|
+
Copyright (c) 2008 FIX
|
37
|
+
|
38
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
39
|
+
a copy of this software and associated documentation files (the
|
40
|
+
'Software'), to deal in the Software without restriction, including
|
41
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
42
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
43
|
+
permit persons to whom the Software is furnished to do so, subject to
|
44
|
+
the following conditions:
|
45
|
+
|
46
|
+
The above copyright notice and this permission notice shall be
|
47
|
+
included in all copies or substantial portions of the Software.
|
48
|
+
|
49
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
50
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
51
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
52
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
53
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
54
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
55
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
require 'rubygems'
|
3
|
+
require './lib/hysteresis_filters.rb'
|
4
|
+
|
5
|
+
desc "Test the hysteresis_filters plugin using rspec"
|
6
|
+
Spec::Rake::SpecTask.new do |t|
|
7
|
+
t.warning = true
|
8
|
+
t.pattern = 'spec/**/*.rb'
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
13
|
+
pkg.need_tar = true
|
14
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
|
4
|
+
#
|
5
|
+
# This Module provides a set of triggers which exhibit the Hysteresis property. Each trigger can be
|
6
|
+
# used by itself or chained togeather to make more complex behaviours. See rspecs for example uses
|
7
|
+
#
|
8
|
+
module HysteresisFilters
|
9
|
+
|
10
|
+
class DecisionFilter
|
11
|
+
attr_accessor :boolean_state
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
self.reset
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset
|
18
|
+
@boolean_state = false
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# A Schmitt Trigger is a 'greather than' test against a two value threshold. When the trigger is in a false state the
|
25
|
+
# the input value must be greater than the 'upper threshold' beforing firing. However when in the true state
|
26
|
+
# the value must dip below the lower_threshold before changing to false. Schmitt Triggers are good for filtering
|
27
|
+
# out oscillations around a threshold. See http://en.wikipedia.org/wiki/Schmitt_trigger
|
28
|
+
#
|
29
|
+
class SchmittTrigger < DecisionFilter
|
30
|
+
|
31
|
+
def greater_than?(value, lower_threshold, upper_threshold)
|
32
|
+
threshold = (@boolean_state) ? lower_threshold : upper_threshold
|
33
|
+
if value > threshold
|
34
|
+
return @boolean_state = true
|
35
|
+
else
|
36
|
+
return @boolean_state = false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# A Trigger than only fires when transitioning between states. In other words the
|
44
|
+
# trigger will return true when the input state changes from false to true, or
|
45
|
+
# vice versa. Useful in cases where you only want to do something the first time
|
46
|
+
# an event happens (such as a Speed Alert email).
|
47
|
+
#
|
48
|
+
class TransitionTrigger < DecisionFilter
|
49
|
+
|
50
|
+
def transitioning?(state)
|
51
|
+
if @boolean_state != state
|
52
|
+
@boolean_state = state
|
53
|
+
return true
|
54
|
+
else
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# A Trigger that only fires after the input state has been continously in that state
|
63
|
+
# for 'count' times. If the input state changes within the last 'count' inputs then
|
64
|
+
# the internal counter is reset. For example, only Fire a Speed Alert email after
|
65
|
+
# been overspeeding continously for 30 seconds.
|
66
|
+
#
|
67
|
+
# The Trigger also implements the method first_occurrence. This can be used to
|
68
|
+
# retrospectively retrieve the first occurrence of the given 'obj' parameter
|
69
|
+
# where the trigger first started to be true.
|
70
|
+
#
|
71
|
+
class ToleranceTrigger < DecisionFilter
|
72
|
+
|
73
|
+
attr_reader :first_occurrence, :count, :value
|
74
|
+
|
75
|
+
def state?(state, count, obj)
|
76
|
+
# only set @value if it's nil (i.e. before trigger has switched)
|
77
|
+
@value ||= obj
|
78
|
+
|
79
|
+
# state unchanged
|
80
|
+
if @boolean_state == state
|
81
|
+
@value = obj
|
82
|
+
# reset
|
83
|
+
@count = 0
|
84
|
+
return @boolean_state
|
85
|
+
# state changed
|
86
|
+
else
|
87
|
+
@count += 1
|
88
|
+
# if first time
|
89
|
+
if @count == 1
|
90
|
+
@possible_first_occurrence = obj
|
91
|
+
end
|
92
|
+
# long enough? then switch over
|
93
|
+
if @count >= count
|
94
|
+
@value = obj
|
95
|
+
@boolean_state = state
|
96
|
+
@first_occurrence = @possible_first_occurrence
|
97
|
+
@count = 0
|
98
|
+
return @boolean_state
|
99
|
+
else
|
100
|
+
return @boolean_state
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
def reset
|
107
|
+
@first_occurrence = nil
|
108
|
+
@count = 0
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'lib/hysteresis_filters'
|
3
|
+
|
4
|
+
describe 'Motion Start-Stop' do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@tt = HysteresisFilters::TransitionTrigger.new
|
8
|
+
@tt.reset
|
9
|
+
@st = HysteresisFilters::ToleranceTrigger.new
|
10
|
+
@st.reset
|
11
|
+
@sct = HysteresisFilters::SchmittTrigger.new
|
12
|
+
@sct.reset
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should filter motion start and stops" do
|
16
|
+
# GPS drift
|
17
|
+
8.times do
|
18
|
+
dist = rand(15)
|
19
|
+
in_motion?(dist).should be_false
|
20
|
+
end
|
21
|
+
|
22
|
+
# False starts
|
23
|
+
in_motion?(19).should be_false
|
24
|
+
in_motion?(19).should be_false
|
25
|
+
in_motion?(3).should be_false
|
26
|
+
in_motion?(19).should be_false
|
27
|
+
in_motion?(3).should be_false
|
28
|
+
|
29
|
+
# Going for real now
|
30
|
+
in_motion?(19, 'a').should be_false
|
31
|
+
in_motion?(19, 'b').should be_false
|
32
|
+
in_motion?(20, 'c').should be_false
|
33
|
+
in_motion?(21, 'd').should be_true
|
34
|
+
in_motion?(17, 'e').should be_true
|
35
|
+
@st.first_occurrence.should == 'a'
|
36
|
+
|
37
|
+
# False stops
|
38
|
+
in_motion?(19).should be_true
|
39
|
+
in_motion?(3).should be_true
|
40
|
+
in_motion?(4).should be_true
|
41
|
+
in_motion?(3).should be_true
|
42
|
+
in_motion?(10).should be_true
|
43
|
+
|
44
|
+
# Stopping for real now
|
45
|
+
in_motion?(11).should be_true
|
46
|
+
in_motion?(3).should be_true
|
47
|
+
in_motion?(3).should be_true
|
48
|
+
in_motion?(4).should be_true
|
49
|
+
in_motion?(3).should be_false
|
50
|
+
in_motion?(15).should be_false
|
51
|
+
in_motion?(0).should be_false
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def in_motion?(dist, obj = nil)
|
58
|
+
@sct.greater_than?(dist, 5, 15)
|
59
|
+
return @sct.boolean_state = @st.state?(@sct.boolean_state, 4, obj)
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'lib/hysteresis_filters'
|
3
|
+
|
4
|
+
describe HysteresisFilters::SchmittTrigger do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@st = HysteresisFilters::SchmittTrigger.new
|
8
|
+
@st.reset
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should return when greater than thresholds" do
|
12
|
+
0.upto(15) do |i|
|
13
|
+
@st.greater_than?(i, 5, 15).should be_false
|
14
|
+
end
|
15
|
+
16.upto(30) do |i|
|
16
|
+
@st.greater_than?(i, 5, 15).should be_true
|
17
|
+
end
|
18
|
+
30.downto(6) do |i|
|
19
|
+
@st.greater_than?(i, 5, 15).should be_true
|
20
|
+
end
|
21
|
+
5.downto(-5) do |i|
|
22
|
+
@st.greater_than?(i, 5, 15).should be_false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'lib/hysteresis_filters'
|
3
|
+
|
4
|
+
describe 'Speed Alert' do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@tt = HysteresisFilters::TransitionTrigger.new
|
8
|
+
@tt.reset
|
9
|
+
@tolt = HysteresisFilters::ToleranceTrigger.new
|
10
|
+
@tolt.reset
|
11
|
+
@sct = HysteresisFilters::SchmittTrigger.new
|
12
|
+
@sct.reset
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should only fire when 1) overspeeding is starting, 2) higher than upper threshold (schmitt), 3) has been speeding for 5 consecutive seconds" do
|
16
|
+
|
17
|
+
# Normal driving
|
18
|
+
100.times do
|
19
|
+
speed = 80 + rand(20)
|
20
|
+
overspeed_started?(speed).should be_false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Not quite speeding
|
24
|
+
overspeed_started?(90).should be_false
|
25
|
+
overspeed_started?(100).should be_false
|
26
|
+
overspeed_started?(110).should be_false
|
27
|
+
overspeed_started?(95).should be_false
|
28
|
+
overspeed_started?(91).should be_false
|
29
|
+
|
30
|
+
# Speeding now
|
31
|
+
overspeed_started?(111, 'a').should be_false
|
32
|
+
overspeed_started?(110, 'b').should be_false
|
33
|
+
overspeed_started?(100, 'c').should be_false
|
34
|
+
overspeed_started?(97, 'd').should be_false
|
35
|
+
# Speed alert fires!!!
|
36
|
+
overspeed_started?(97, 'e').should be_true
|
37
|
+
@tolt.first_occurrence.should == 'a'
|
38
|
+
|
39
|
+
# Continuing to speed, but not re-triggering
|
40
|
+
overspeed_started?(97).should be_false
|
41
|
+
overspeed_started?(110).should be_false
|
42
|
+
overspeed_started?(100).should be_false
|
43
|
+
overspeed_started?(97).should be_false
|
44
|
+
overspeed_started?(100).should be_false
|
45
|
+
|
46
|
+
# Slowing down, but not long enough to reset trigger
|
47
|
+
overspeed_started?(93).should be_false
|
48
|
+
overspeed_started?(90).should be_false
|
49
|
+
|
50
|
+
# Speeding again
|
51
|
+
overspeed_started?(101).should be_false
|
52
|
+
overspeed_started?(110).should be_false
|
53
|
+
|
54
|
+
# Slowing down. Now trigger is reset
|
55
|
+
overspeed_started?(93).should be_false
|
56
|
+
overspeed_started?(90).should be_false
|
57
|
+
overspeed_started?(82).should be_false
|
58
|
+
overspeed_started?(80).should be_false
|
59
|
+
overspeed_started?(97).should be_false
|
60
|
+
|
61
|
+
# Speeding again. This time fire.
|
62
|
+
overspeed_started?(101).should be_false
|
63
|
+
overspeed_started?(110).should be_false
|
64
|
+
overspeed_started?(100).should be_false
|
65
|
+
overspeed_started?(97).should be_false
|
66
|
+
# Speed alert fires!!!
|
67
|
+
overspeed_started?(97).should be_true
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
def overspeed_started?(speed, obj = nil)
|
74
|
+
@result = @sct.greater_than?(speed, 95, 100)
|
75
|
+
@result = @tolt.state?(@sct.boolean_state, 5, obj)
|
76
|
+
@tt.transitioning?(@result) and @result
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'lib/hysteresis_filters'
|
3
|
+
|
4
|
+
describe HysteresisFilters::ToleranceTrigger do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@st = HysteresisFilters::ToleranceTrigger.new
|
8
|
+
@st.reset
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should return true only after condition has been true >10 times" do
|
12
|
+
0.upto(30) do |i|
|
13
|
+
if i < 24
|
14
|
+
@st.state?(i > 14, 10, i).should be_false
|
15
|
+
else
|
16
|
+
@st.state?(i > 14, 10, i).should be_true
|
17
|
+
@st.first_occurrence.should == 15
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'lib/hysteresis_filters'
|
3
|
+
|
4
|
+
describe HysteresisFilters::TransitionTrigger do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@tt = HysteresisFilters::TransitionTrigger.new
|
8
|
+
@tt.reset
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should return true transitioning between states" do
|
12
|
+
0.upto(30) do |i|
|
13
|
+
if i != 16
|
14
|
+
@tt.transitioning?(i > 15).should be_false
|
15
|
+
else
|
16
|
+
@tt.transitioning?(i > 15).should be_true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
30.downto(-5) do |i|
|
20
|
+
if i != 15
|
21
|
+
@tt.transitioning?(i > 15).should be_false
|
22
|
+
else
|
23
|
+
@tt.transitioning?(i > 15).should be_true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aishafenton-hysteresis_filters
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- VisFleet
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-12-09 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A collection of simple decision filters which have the Hysteresis property
|
17
|
+
email: info@visfleet.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.txt
|
24
|
+
files:
|
25
|
+
- lib/hysteresis_filters.rb
|
26
|
+
- Rakefile
|
27
|
+
- README.txt
|
28
|
+
- spec/motion_start_spec.rb
|
29
|
+
- spec/schmitt_trigger_spec.rb
|
30
|
+
- spec/speed_alert_spec.rb
|
31
|
+
- spec/tolerance_trigger_spec.rb
|
32
|
+
- spec/transition_trigger_spec.rb
|
33
|
+
has_rdoc: true
|
34
|
+
homepage:
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.2.0
|
56
|
+
signing_key:
|
57
|
+
specification_version: 2
|
58
|
+
summary: "A collection of simple decision filters which have the Hysteresis property. The filters provide simple boolean logic but with a little added fuzzyness. The fuzzyness is based on what states a filter has previously been through (i.e. Hysteresis). These filters are useful when you want to: smooth away brief changes in state (e.g. Tolerance filter), make your logic robust against flipping between states (e.g. Schmitt filter), or only want to know when something has changed (e.g. Transition filter)."
|
59
|
+
test_files: []
|
60
|
+
|