interval_notation 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ require_relative 'segmentation'
2
+ require_relative 'sweep_line/trace_states'
3
+ require_relative 'sweep_line/sweep_line'
@@ -0,0 +1,97 @@
1
+ require_relative '../segmentation'
2
+
3
+ module IntervalNotation
4
+ # SweepLine is an internal helper module for combining interval sets using sweep line.
5
+ # It starts moving from -∞ to +∞ and keep which intervals are crossed by sweep line in trace-state variable.
6
+ # State object have methods which recalculate state when sweep line meets a group of boundary points
7
+ # and a method to convolve state underlying representation into surface state value.
8
+ # E.g. `Union` trace state can store information about which intervals are intersected by a sweep line,
9
+ # but on surface one knows only whether it intersects any interval.
10
+ #
11
+ # Boundary points for state recalculation are grouped by the same coordinate. As sweep lines goes left-to-right,
12
+ # initial state is a state at -∞.
13
+ # See `TraceState` module for an examples of trace-states.
14
+ #
15
+ # Principially sweep line method helps to effectively recalculate number of crossed intervals without rechecking
16
+ # all intervals each time, and dramatically reduces speed of operations on large number of intervals.
17
+ #
18
+ # Usage example:
19
+ # SweepLine.make_interval_set([interval_1, interval_2, interval_3], SweepLine::TraceState::Union.initial_state(3))
20
+ #
21
+ module SweepLine
22
+ # Make segmentation by state of a list of interval sets.
23
+ # Accepts a list of pairs (interval, tag)
24
+ def self.make_segmentation(indexed_interval_sets, initial_state)
25
+ points = interval_boundaries(indexed_interval_sets)
26
+ segmentation_by_boundary_points(points, initial_state).map_state{|segment|
27
+ segment.state.state_convolution
28
+ }
29
+ end
30
+
31
+ # Make segmentation by state of a list of interval sets.
32
+ # Accepts a list of intervals without tags.
33
+ def self.make_interval_set(interval_sets, initial_state)
34
+ make_segmentation(interval_sets.each_with_index, initial_state).make_interval_set
35
+ end
36
+
37
+ # Make tagging segmentation of interval sets.
38
+ # Each segment's state in a resulting segmentation is a set of tags lying against a segment.
39
+ def self.make_tagging(indexed_interval_sets)
40
+ make_segmentation(indexed_interval_sets, SweepLine::TraceState::SingleTagging.initial_state)
41
+ end
42
+
43
+ # Make multi-tagging segmentation of interval sets. Multi-tagging means that segment store
44
+ # not only tag but number of times this tag was met.
45
+ # Each segment's state in a resulting segmentation is a hash {tag => count} for tags lying against a segment.
46
+ def self.make_multitagging(indexed_interval_sets)
47
+ make_segmentation(indexed_interval_sets, SweepLine::TraceState::MultiTagging.initial_state)
48
+ end
49
+
50
+ # Extracts interval boundaries marked by interval indices or tags
51
+ # Accepts a list of pairs (interval, tag)
52
+ def self.interval_boundaries(tagged_interval_sets)
53
+ tagged_interval_sets.flat_map{|interval_set, interval_set_tag|
54
+ interval_set.intervals.flat_map{|interval|
55
+ interval.interval_boundaries(interval_set_tag)
56
+ }
57
+ }
58
+ end
59
+
60
+ # Make a segmentation using sweep line along boundary points.
61
+ def self.segmentation_by_boundary_points(boundary_points, initial_state)
62
+ if boundary_points.empty?
63
+ segment = Segmentation::Segment.new(BasicIntervals::OpenOpenInterval.new(-Float::INFINITY, Float::INFINITY), initial_state)
64
+ return Segmentation.new([segment])
65
+ end
66
+
67
+ point_chunks = boundary_points.sort_by(&:value).chunk(&:value).to_a
68
+ state = initial_state
69
+
70
+ # Process minus-infinity points which can change initial state
71
+ if point_chunks.first.first == -Float::INFINITY
72
+ point_value, points_at_minus_infinity = point_chunks.shift
73
+ state = state.state_after_point(points_at_minus_infinity)
74
+ end
75
+
76
+ # Remove plus-infinity points as they can change state but this won't be reflected by any interval
77
+ if point_chunks.last.first == Float::INFINITY
78
+ point_value, points_at_plus_infinity = point_chunks.pop
79
+ end
80
+
81
+ prev_point_value = -Float::INFINITY
82
+ segments = []
83
+ # We removed points at plus or minus infinity so now we process inner points
84
+ point_chunks.each do |point_value, points_on_place|
85
+ segments << Segmentation::Segment.new(BasicIntervals::OpenOpenInterval.new(prev_point_value, point_value), state)
86
+ segments << Segmentation::Segment.new(BasicIntervals::Point.new(point_value), state.state_at_point(points_on_place))
87
+ state = state.state_after_point(points_on_place)
88
+ prev_point_value = point_value
89
+ end
90
+
91
+ # add fictive segment up to plus-infinity point (may be fictive)
92
+ segments << Segmentation::Segment.new(BasicIntervals::OpenOpenInterval.new(prev_point_value, Float::INFINITY), state)
93
+ Segmentation.new(segments, skip_validation: true) # here we can skip validation but not normalization
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,56 @@
1
+ module IntervalNotation
2
+ module SweepLine
3
+ module TraceState
4
+
5
+ # Class allows to observe whether sweep line is inside of interval sets intersection or outside
6
+ Intersection = Struct.new(:num_uncovered) do
7
+ def self.initial_state(num_interval_sets)
8
+ self.new(num_interval_sets)
9
+ end
10
+
11
+ # map state before point into state at point
12
+ def state_at_point(points_on_place)
13
+ new_state = num_uncovered
14
+ points_on_place.each{|point|
15
+ if point.singular_point?
16
+ new_state -= 1
17
+ else
18
+ if point.closing && !point.included
19
+ new_state += 1
20
+ elsif point.opening && point.included
21
+ new_state -= 1
22
+ end
23
+ end
24
+ }
25
+ self.class.new(new_state)
26
+ end
27
+
28
+ # map state at point into state after point
29
+ def state_after_point(points_on_place)
30
+ new_state = num_uncovered
31
+ points_on_place.reject(&:singular_point?).each{|point|
32
+ new_state += point.opening ? -1 : +1
33
+ }
34
+
35
+ self.class.new(new_state)
36
+ end
37
+
38
+ def state_convolution
39
+ num_uncovered == 0
40
+ end
41
+ end
42
+
43
+ # # More generic but less efficient version of Intersection state
44
+ # class IntersectionMultiState < MultipleState
45
+ # def self.initial_state(num_interval_sets)
46
+ # self.new( Array.new(num_interval_sets, false) )
47
+ # end
48
+
49
+ # def state_convolution
50
+ # inclusion_state.all?
51
+ # end
52
+ # end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ module IntervalNotation
2
+ module SweepLine
3
+ module TraceState
4
+
5
+ # MultipleState is a simple abstract class to store and manage state of intersection with sweep line of several interval sets
6
+ #
7
+ # In order to use it one should define subclass with `#state_convolution` method.
8
+ # If specific convolution can be defined in easier terms, `#state_at_point` and `#state_after_point`
9
+ # also can be redefined in a subclass for perfomance and clarity reasons
10
+ MultipleState = Struct.new(:inclusion_state) do
11
+ # map state before point into state at point
12
+ def state_at_point(points_on_place)
13
+ new_state = inclusion_state.dup
14
+ points_on_place.each{|point|
15
+ new_state[point.interval_index] = point.included
16
+ }
17
+ self.class.new(new_state)
18
+ end
19
+
20
+ # map state before point (not at point!) into state after point
21
+ def state_after_point(points_on_place)
22
+ new_state = inclusion_state.dup
23
+
24
+ interval_boundary_points = points_on_place.reject(&:singular_point?)
25
+ points_by_closing = interval_boundary_points.group_by(&:closing)
26
+ closing_points = points_by_closing.fetch(true){ [] }
27
+ opening_points = points_by_closing.fetch(false){ [] }
28
+
29
+ closing_points.each{|point|
30
+ new_state[point.interval_index] = false
31
+ }
32
+ opening_points.each{|point|
33
+ new_state[point.interval_index] = true
34
+ }
35
+ self.class.new(new_state)
36
+ end
37
+
38
+ # Convolve state inner state into state result
39
+ def state_convolution
40
+ raise NotImplementedError, '#state_convolution should be redefined in a superclass'
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'multiple_state'
2
+
3
+ module IntervalNotation
4
+ module SweepLine
5
+ module TraceState
6
+
7
+ # Class allows to observe whether sweep line is inside of first and outside of second interval set
8
+ class Subtract < MultipleState
9
+ def self.initial_state
10
+ self.new([false, false])
11
+ end
12
+
13
+ def state_convolution
14
+ inclusion_state[0] && !inclusion_state[1]
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'multiple_state'
2
+
3
+ module IntervalNotation
4
+ module SweepLine
5
+ module TraceState
6
+
7
+ # Class allows to observe whether sweep line is inside of exactly one of two intervals
8
+ class SymmetricDifference < MultipleState
9
+ def self.initial_state
10
+ self.new([false, false])
11
+ end
12
+
13
+ def state_convolution
14
+ inclusion_state[0] ^ inclusion_state[1]
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,72 @@
1
+ require 'set'
2
+
3
+ module IntervalNotation
4
+ module SweepLine
5
+ module TraceState
6
+
7
+ # Tagging is an abstract state class which stores an overlay markup of several tagged interval-sets.
8
+ # Look at SingleTagging and MultiTagging which implement different convolution strategies (see below).
9
+ #
10
+ # Each interval set can be marked with the only tag but for an overlay each individual point
11
+ # can be marked with zero to many tags.
12
+ # Tag name is taken from boundary point's interval index.
13
+ # Tag names needn't be unique.
14
+ #
15
+ # When a point is overlapped by several tags with the same name, there are two strategies:
16
+ # * SingleTagging takes into account only the fact, that the point/interval was tagged
17
+ # * MultiTagging count the number of times each tag was assigned to a point/interval
18
+ #
19
+ Tagging = Struct.new(:tag_count) do
20
+ def self.initial_state
21
+ self.new(Hash.new(0))
22
+ end
23
+
24
+ # map state before point into state at point
25
+ def state_at_point(points_on_place)
26
+ new_state = tag_count.dup
27
+ points_on_place.each{|point|
28
+ if point.singular_point?
29
+ new_state[point.interval_index] += 1
30
+ else
31
+ if point.closing && !point.included
32
+ new_state[point.interval_index] -= 1
33
+ elsif point.opening && point.included
34
+ new_state[point.interval_index] += 1
35
+ end
36
+ end
37
+ }
38
+ new_state.reject!{|tag, count| count.zero?}
39
+ self.class.new(new_state)
40
+ end
41
+
42
+ # map state before point (not at point!) into state after point
43
+ def state_after_point(points_on_place)
44
+ new_state = tag_count.dup
45
+ points_on_place.reject(&:singular_point?).each{|point|
46
+ new_state[point.interval_index] += point.opening ? 1 : -1
47
+ }
48
+ new_state.reject!{|tag, count| count.zero?}
49
+ self.class.new(new_state)
50
+ end
51
+
52
+ # Convolve state inner state into state result
53
+ def state_convolution
54
+ raise NotImplementedError, 'Tagging is an abstract state class. Use SingleTagging or MultiTagging instead'
55
+ end
56
+ end
57
+
58
+ class SingleTagging < Tagging
59
+ def state_convolution; tag_count.keys.to_set; end
60
+ def to_s; "{#{tag_count.keys.join(', ')}}"; end
61
+ def inspect; to_s; end
62
+ end
63
+
64
+ class MultiTagging < Tagging
65
+ def state_convolution; tag_count; end
66
+ def to_s; tag_count.to_s; end
67
+ def inspect; to_s; end
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,57 @@
1
+ module IntervalNotation
2
+ module SweepLine
3
+ module TraceState
4
+
5
+ # Class allows to observe whether sweep line is inside of interval sets union or outside
6
+ Union = Struct.new(:num_covered) do
7
+ def self.initial_state(num_interval_sets)
8
+ self.new(0)
9
+ end
10
+
11
+ # map state before point into state at point
12
+ def state_at_point(points_on_place)
13
+ new_state = num_covered
14
+ points_on_place.each{|point|
15
+ if point.singular_point?
16
+ new_state += 1
17
+ else
18
+ if point.closing && !point.included
19
+ new_state -= 1
20
+ elsif point.opening && point.included
21
+ new_state += 1
22
+ end
23
+ end
24
+ }
25
+ self.class.new(new_state)
26
+ end
27
+
28
+ # map state at point into state after point
29
+ def state_after_point(points_on_place)
30
+ new_state = num_covered
31
+ points_on_place.reject(&:singular_point?).each{|point|
32
+ new_state += point.opening ? 1 : -1
33
+ }
34
+
35
+ self.class.new(new_state)
36
+ end
37
+
38
+ def state_convolution
39
+ num_covered > 0
40
+ end
41
+ end
42
+
43
+ # # More generic but less efficient version of Union state
44
+ # require_relative 'multiple_state'
45
+ # class Union < MultipleState
46
+ # def self.initial_state(num_interval_sets)
47
+ # self.new( Array.new(num_interval_sets, false) )
48
+ # end
49
+
50
+ # def state_convolution
51
+ # inclusion_state.any?
52
+ # end
53
+ # end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,6 @@
1
+ require_relative 'trace_state/multiple_state'
2
+ require_relative 'trace_state/tagging'
3
+ require_relative 'trace_state/union'
4
+ require_relative 'trace_state/intersection'
5
+ require_relative 'trace_state/subtract'
6
+ require_relative 'trace_state/symmetric_difference'
@@ -1,3 +1,3 @@
1
1
  module IntervalNotation
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -736,5 +736,47 @@ describe IntervalNotation do
736
736
  expect(interval.closure).to eq answer
737
737
  end
738
738
  end
739
+
740
+ describe '#connected_components' do
741
+ {
742
+ Empty => [],
743
+ oo(1,3) => [oo(1,3)],
744
+ cc(5,6) => [cc(5,6)],
745
+ pt(4) => [pt(4)],
746
+ oo(1,3) | pt(4) => [oo(1,3), pt(4)],
747
+ oo(1,3) | cc(5,6) => [oo(1,3), cc(5,6)],
748
+ oo(1,3) | pt(4) | cc(5,6) => [oo(1,3), pt(4), cc(5,6)],
749
+ }.each do |interval, answer|
750
+ it "#{interval}.connected_components should equal #{answer}" do
751
+ expect(interval.connected_components).to eq answer
752
+ end
753
+ end
754
+ end
755
+
756
+ describe '#interval_covering_point' do
757
+ {
758
+ [Empty, 2] => nil,
759
+ [oo(1,3), 2] => OpenOpenInterval.new(1,3),
760
+ [cc(1,3), 2] => ClosedClosedInterval.new(1,3),
761
+ [oo(1,3), 4] => nil,
762
+ [oo(1,3), 3] => nil,
763
+ [oo(1,3), 1] => nil,
764
+ [cc(1,3), 1] => ClosedClosedInterval.new(1,3),
765
+ [cc(1,3), 3] => ClosedClosedInterval.new(1,3),
766
+ [pt(4), 5] => nil,
767
+ [pt(4), 4] => Point.new(4),
768
+ [oo(1,3) | pt(4), 4] => Point.new(4),
769
+ [oo(1,3) | oo(5,6), 4] => nil,
770
+ [oo(1,3) | oo(5,6), 2] => OpenOpenInterval.new(1,3),
771
+ [oo(1,3) | oo(5,6), 5.5] => OpenOpenInterval.new(5,6),
772
+ [oo(1,3) | pt(4) | oo(5,6), 4] => Point.new(4),
773
+ [oo(1,3) | pt(4) | oo(5,6), 2] => OpenOpenInterval.new(1,3),
774
+ [oo(1,3) | pt(4) | oo(5,6), 5.5] => OpenOpenInterval.new(5,6),
775
+ }.each do |(interval, point), answer|
776
+ it "#{interval}.interval_covering_point(#{point}) should equal #{answer}" do
777
+ expect(interval.interval_covering_point(point)).to eq answer
778
+ end
779
+ end
780
+ end
739
781
  end
740
782
  end