dither 0.1.5 → 0.2.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ /*
2
+ *
3
+ * Copyright (C) 2015 Jason Gowan
4
+ * All rights reserved.
5
+ *
6
+ * This software may be modified and distributed under the terms
7
+ * of the BSD license. See the LICENSE file for details.
8
+ */
9
+
10
+ #ifndef SIMPLE_CONSTRAINT_HANDLER_H_
11
+ #define SIMPLE_CONSTRAINT_HANDLER_H_
12
+
13
+ #include <vector>
14
+ #include <utility>
15
+ #include <algorithm>
16
+ #include "dither_types.h"
17
+ #include "base_constraint_handler.h"
18
+
19
+ namespace dither {
20
+
21
+ class SimpleConstraintHandler : public BaseConstraintHandler {
22
+ protected:
23
+ std::vector<std::vector<std::pair<std::size_t, dval>>> constraints;
24
+ std::vector<dval> params;
25
+
26
+ inline bool violate_constraint(const dtest_case& test_case, const std::vector<std::pair<std::size_t, dval>>& constraint);
27
+ inline bool violate_constraint(const std::vector<param>& test_case, const std::vector<std::pair<std::size_t, dval>>& constraint);
28
+
29
+ public:
30
+ SimpleConstraintHandler(std::vector<dval>& ranges, std::vector<std::vector<dval>>& pconstraints);
31
+ bool violate_constraints(const dtest_case &test_case);
32
+ bool violate_constraints(const std::vector<param> &test_case);
33
+ bool ground(dtest_case &test_case);
34
+ };
35
+ }
36
+
37
+ #endif // SIMPLE_CONSTRAINT_HANDLER_H_
38
+
data/lib/dither/api.rb ADDED
@@ -0,0 +1,20 @@
1
+
2
+ require 'ffi'
3
+
4
+ # Interface to the c++ api.
5
+ module Dither
6
+ module API
7
+ extend FFI::Library
8
+ ffi_lib %w[lib/dither.so lib/dither.dll]
9
+
10
+ attach_function :dither_ipog_new, [:int], :pointer
11
+ attach_function :dither_ipog_add_parameter_int, [:pointer, :int, :pointer, :int], :void
12
+ attach_function :dither_ipog_run, [:pointer], :void
13
+ attach_function :dither_ipog_size, [:pointer], :int
14
+ attach_function :dither_ipog_display_raw_solution, [:pointer], :void
15
+ attach_function :dither_ipog_fill, [:pointer, :pointer], :void
16
+ attach_function :dither_ipog_add_constraint, [:pointer, :pointer, :int], :void
17
+ attach_function :dither_ipog_add_previously_tested, [:pointer, :pointer, :int], :void
18
+ # attach_function :dither_ipog_delete, [:pointer], :void
19
+ end
20
+ end
@@ -1,4 +1,4 @@
1
1
 
2
2
  module Dither
3
- VERSION = '0.1.5'
3
+ VERSION = '0.2.0.rc3'
4
4
  end
data/lib/dither.rb CHANGED
@@ -12,18 +12,73 @@ module Dither
12
12
  # deprecated
13
13
  def self.all_pairs(params, t = 2, opts = {})
14
14
  opts[:t] = t
15
- IPOG.new(params, opts).run
15
+ ipog(params, opts)
16
16
  end
17
17
 
18
18
  def self.ipog(params, opts = {})
19
19
  opts = DEFUALT_OPTS.dup.merge(opts)
20
- IPOG.new(params, opts).run
21
- end
20
+ t = opts[:t] || 2
21
+ if t < 2
22
+ raise Dither::Error,'t must be >= 2'
23
+ end
24
+ raise Dither::Error, 'param length must be > 1' if params.any? { |a| a.size <= 1 }
25
+ if t > params.size
26
+ raise Dither::Error, 't must be <= params.length'
27
+ end
22
28
 
23
- def self.mipog(params, t = 2, opts = {})
24
- raise Error, 'mipog does not support constraints' if opts.key?(:constraints)
25
- opts[:t] = t
26
- MIPOG.new(params, opts).run
29
+ pointer = Dither::API.dither_ipog_new(t)
30
+ c_params = (0..params.max { |a| a.size }.size).to_a
31
+ c_int_params = FFI::MemoryPointer.new(:int, c_params.size)
32
+ c_int_params.write_array_of_int(c_params)
33
+
34
+ params.each_with_index do |param, i|
35
+ Dither::API.dither_ipog_add_parameter_int(pointer, i, c_int_params, param.size)
36
+ end
37
+
38
+ if opts[:constraints]
39
+ constraint_scratch = FFI::MemoryPointer.new(:int, params.size)
40
+ opts[:constraints].each do |constraint|
41
+ arr = Array.new(params.size, -1)
42
+ constraint.each do |k, v|
43
+ if k >= params.size
44
+ raise Dither::Error, "Invalid constraint #{k} > #{params.size}"
45
+ end
46
+ if v >= params[k].size
47
+ raise Dither::Error, "Invalid constraint #{k} > #{params[k].size}"
48
+
49
+ end
50
+ arr[k] = v
51
+ end
52
+ constraint_scratch.write_array_of_int(arr)
53
+ Dither::API.dither_ipog_add_constraint(pointer, constraint_scratch, params.size)
54
+ end
55
+ end
56
+
57
+ if opts[:previously_tested]
58
+ tested_scratch = FFI::MemoryPointer.new(:int, params.size)
59
+ opts[:previously_tested].each do |test_case|
60
+ if test_case.size != params.size
61
+ raise Dither::Error
62
+ end
63
+ arr = Array.new(params.size)
64
+ (0...params.size).each do |i|
65
+ arr[i] = params[i].find(test_case[i]).first
66
+ end
67
+ tested_scratch.write_array_of_int(arr)
68
+ Dither::API.dither_ipog_add_previously_tested(pointer, tested_scratch, params.size)
69
+ end
70
+ end
71
+
72
+ Dither::API.dither_ipog_run(pointer)
73
+ result_size = Dither::API.dither_ipog_size(pointer)
74
+ solution = FFI::MemoryPointer.new(:int, params.size * result_size)
75
+ Dither::API.dither_ipog_fill(pointer, solution)
76
+
77
+ results = solution.read_array_of_int(params.size * result_size)
78
+ .enum_for(:each_slice, params.size)
79
+ .map do |test_case|
80
+ test_case.zip(params).map { |a, b| b[a] }
81
+ end
27
82
  end
28
83
 
29
84
  def self.aetg(params, opts = {})
@@ -34,12 +89,6 @@ module Dither
34
89
  class << self; alias_method :ateg, :aetg end
35
90
  end # Dither
36
91
 
37
- require 'dither/param'
38
- require 'dither/unbound_param'
39
- require 'dither/test_case'
40
- require 'dither/ipog_helper'
41
- require 'dither/ipog'
42
- require 'dither/mipog'
43
92
  require 'dither/chinese_postman_problem'
44
93
  require 'dither/aetg'
45
94
  require 'dither/aetg_pairwise'
@@ -51,4 +100,6 @@ if RUBY_PLATFORM =~ /java/
51
100
  require 'dither-0.1.3.jar'
52
101
 
53
102
  require 'dither/java_ext/dither'
103
+ else
104
+ require 'dither/api'
54
105
  end
@@ -2,10 +2,6 @@ require File.expand_path('../../spec_helper.rb', __FILE__)
2
2
 
3
3
  describe Dither do
4
4
 
5
- it 'mipog does not support constraints' do
6
- expect { Dither.mipog([[1,1],[1,2]], 2, :constraints => []) }.to raise_error(Dither::Error, 'mipog does not support constraints')
7
- end
8
-
9
5
  it 't must be >= 2' do
10
6
  expect { Dither.ipog([], :t => 0) }.to raise_error(Dither::Error, 't must be >= 2')
11
7
  end
@@ -20,63 +16,15 @@ describe Dither do
20
16
 
21
17
  it 'can compute 2-way ipog using symbols' do
22
18
  params = [[:a, :b, :c], [:d, :e, :f], [:h, :i]]
23
- expect(Dither.ipog(params)).to eq([[:a, :d, :h],
24
- [:a, :e, :i],
25
- [:a, :f, :h],
26
- [:b, :d, :i],
27
- [:b, :e, :h],
28
- [:b, :f, :i],
29
- [:c, :d, :h],
30
- [:c, :e, :i],
31
- [:c, :f, :h]])
32
- end
33
-
34
- it 'can compute 3-way mipog' do
35
- params = [(0...2).to_a, (0...2).to_a, (0..3).to_a]
36
- expect(Dither.mipog(params, 3)).to eq([[0, 0, 0],
37
- [1, 0, 0],
38
- [0, 1, 0],
39
- [1, 1, 0],
40
- [0, 0, 1],
41
- [1, 0, 1],
42
- [0, 1, 1],
43
- [1, 1, 1],
44
- [0, 0, 2],
45
- [1, 0, 2],
46
- [0, 1, 2],
47
- [1, 1, 2],
48
- [0, 0, 3],
49
- [1, 0, 3],
50
- [0, 1, 3],
51
- [1, 1, 3],
52
- ])
53
- end
54
-
55
- it 'can compute 2-way mipog using symbols' do
56
- params = [[:a, :b, :c], [:d, :e, :f], [:h, :i]]
57
- expect(Dither.mipog(params).to_set).to eq([[:a, :d, :h],
58
- [:a, :e, :i],
59
- [:a, :f, :h],
60
- [:b, :d, :i],
61
- [:b, :e, :h],
62
- [:b, :f, :i],
63
- [:c, :d, :h],
64
- [:c, :e, :i],
65
- [:c, :f, :h]].to_set)
66
- end
67
-
68
- it 'can compute 2-way mipog' do
69
- params = [(0...2).to_a, (0..3).to_a]
70
- expect(Dither.mipog(params)).to eq([
71
- [0, 0],
72
- [1, 0],
73
- [0, 1],
74
- [1, 1],
75
- [0, 2],
76
- [1, 2],
77
- [0, 3],
78
- [1, 3],
79
- ])
19
+ expect(Dither.all_pairs(params)).to eq([[:c, :f, :h],
20
+ [:b, :f, :i],
21
+ [:a, :f, :h],
22
+ [:c, :e, :i],
23
+ [:b, :e, :h],
24
+ [:a, :e, :i],
25
+ [:c, :d, :h],
26
+ [:b, :d, :i],
27
+ [:a, :d, :h]])
80
28
  end
81
29
 
82
30
  it 'can compute 2-way ipog' do
@@ -90,7 +38,7 @@ describe Dither do
90
38
  [1, 2],
91
39
  [0, 3],
92
40
  [1, 3],
93
- ])
41
+ ].reverse)
94
42
  end
95
43
 
96
44
  it 'can compute 3-way ipog' do
@@ -116,54 +64,37 @@ describe Dither do
116
64
 
117
65
  it 'can compute 3-way ipog with constraints' do
118
66
  params = [(0...2).to_a, (0...2).to_a, (0..3).to_a]
119
- expect(Dither.ipog(params, :t => 3,
67
+ results = Dither.ipog(params, :t => 3,
120
68
  :constraints => [
121
69
  {0 => 0,
122
70
  2 => 2},
123
71
  {0 => 0,
124
72
  1 => 1,
125
73
  2 => 0}],
126
- :previously_tested => [[0, 0, 0]]).to_set).to eq([
127
- [1, 0, 0],
128
- [1, 1, 0],
129
- [0, 0, 1],
130
- [1, 0, 1],
131
- [0, 1, 1],
132
- [1, 1, 1],
133
- [1, 0, 2],
134
- [1, 1, 2],
135
- [0, 0, 3],
136
- [1, 0, 3],
137
- [0, 1, 3],
138
- [1, 1, 3],
139
- ].to_set)
74
+ :previously_tested => [[0, 0, 0]]
75
+ )
76
+ results.each do |result|
77
+ expect(result[0] == 0 && result[1] == 1 && result[2] == 0).to be false
78
+ end
79
+ results.each do |result|
80
+ expect(result[0] == 0 && result[1] == 2).to be false
81
+ end
82
+ results.each do |result|
83
+ expect(result[0] == 0 && result[1] == 0 && result[2] == 0).to be false
84
+ end
140
85
  end
141
86
 
142
87
  it 'another 3-way ipog with constraints' do
143
88
  params = [(0...2).to_a, (0...2).to_a, (0...2).to_a, (0..3).to_a]
144
- expect(Dither.ipog(params, :t => 3,
89
+ results = Dither.ipog(params, :t => 3,
145
90
  :constraints => [
146
91
  {0 => 0,
147
92
  1 => 1,
148
93
  2 => 0}
149
- ]).to_set).to eq([[0, 0, 0, 0],
150
- [1, 1, 0, 0],
151
- [1, 0, 1, 0],
152
- [0, 1, 1, 0],
153
- [1, 0, 0, 1],
154
- [1, 1, 0, 1],
155
- [0, 0, 1, 1],
156
- [1, 1, 1, 1],
157
- [0, 0, 0, 2],
158
- [1, 1, 0, 2],
159
- [1, 0, 1, 2],
160
- [0, 1, 1, 2],
161
- [0, 0, 0, 3],
162
- [1, 1, 0, 3],
163
- [1, 0, 1, 3],
164
- [0, 1, 1, 3],
165
- [0, 0, 0, 1],
166
- [0, 1, 1, 1]].to_set)
94
+ ])
95
+ results.each do |result|
96
+ expect(result[0] == 0 && result[1] == 1 && result[2] == 0).to be false
97
+ end
167
98
  end
168
99
 
169
100
  it 'can run 2-way aetg' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dither
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Gowan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-21 00:00:00.000000000 Z
11
+ date: 2015-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.9.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake-compiler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: coveralls
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,11 +66,26 @@ dependencies:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ffi
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
55
83
  description: Efficient test generation strategies
56
84
  email:
57
85
  - gowanjason@gmail.com
58
86
  executables: []
59
- extensions: []
87
+ extensions:
88
+ - ext/dither/extconf.rb
60
89
  extra_rdoc_files: []
61
90
  files:
62
91
  - ".gitignore"
@@ -68,18 +97,24 @@ files:
68
97
  - README.md
69
98
  - Rakefile
70
99
  - dither.gemspec
100
+ - ext/dither/README.md
101
+ - ext/dither/base_constraint_handler.h
102
+ - ext/dither/combinations.h
103
+ - ext/dither/dither.cc
104
+ - ext/dither/dither.h
105
+ - ext/dither/dither_types.h
106
+ - ext/dither/extconf.rb
107
+ - ext/dither/ipog.cc
108
+ - ext/dither/ipog.h
109
+ - ext/dither/simple_constraint_handler.cc
110
+ - ext/dither/simple_constraint_handler.h
71
111
  - lib/dither.rb
72
112
  - lib/dither/aetg.rb
73
113
  - lib/dither/aetg_pairwise.rb
114
+ - lib/dither/api.rb
74
115
  - lib/dither/chinese_postman_problem.rb
75
116
  - lib/dither/graph.rb
76
- - lib/dither/ipog.rb
77
- - lib/dither/ipog_helper.rb
78
117
  - lib/dither/java_ext/dither.rb
79
- - lib/dither/mipog.rb
80
- - lib/dither/param.rb
81
- - lib/dither/test_case.rb
82
- - lib/dither/unbound_param.rb
83
118
  - lib/dither/version.rb
84
119
  - spec/dither/chinese_postman_problem_spec.rb
85
120
  - spec/dither/dither_spec.rb
@@ -100,12 +135,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
100
135
  version: '0'
101
136
  required_rubygems_version: !ruby/object:Gem::Requirement
102
137
  requirements:
103
- - - ">="
138
+ - - ">"
104
139
  - !ruby/object:Gem::Version
105
- version: '0'
140
+ version: 1.3.1
106
141
  requirements: []
107
142
  rubyforge_project: dither
108
- rubygems_version: 2.2.0
143
+ rubygems_version: 2.4.5.1
109
144
  signing_key:
110
145
  specification_version: 4
111
146
  summary: Collection of test generation strategies
data/lib/dither/ipog.rb DELETED
@@ -1,58 +0,0 @@
1
- # coding: utf-8
2
-
3
- module Dither
4
- class IPOG
5
- include IPOGHelper
6
-
7
- def run
8
- # add into test set a test for each combination of values
9
- # of the first t parameter
10
- test_set = comb
11
-
12
- (t...params.length).each do |i|
13
- # let pi
14
- # be the set of t-way combinations of values involving
15
- # parameter Pi and t -1 parameters among the first i – 1
16
- # parameters
17
- pi = comb_i(i)
18
-
19
- # horizontal extension for parameter i
20
- test_set.each do |test_case|
21
- cover = maximize_coverage(i, test_case, pi)
22
-
23
- if cover.nil?
24
- test_set.delete(test_case)
25
- else
26
- pi -= cover
27
- end
28
- end
29
-
30
- # vertical extension for parameter i
31
- pi.each do |a|
32
- if test_set.any? { |b| a.subset?(b) }
33
- pi.delete(a)
34
- else
35
-
36
- test_case = nil
37
- test_set.each do |b|
38
- test_case = b.merge_without_conflict(i, a) do |a|
39
- violates_constraints?(a)
40
- end
41
- break unless test_case.nil?
42
- end
43
-
44
- if test_case.nil?
45
- test_set << a.create_unbound(i)
46
- end
47
- pi.delete(a)
48
- end
49
- end
50
- end
51
-
52
- @test_set = test_set.map { |a| fill_unbound(a) }
53
- .delete_if(&:nil?)
54
- .to_a
55
- @test_set
56
- end
57
- end # IPOG
58
- end # Dither
@@ -1,161 +0,0 @@
1
- # coding: utf-8
2
-
3
- module Dither
4
- module IPOGHelper
5
- attr_reader :params, :t, :constraints, :test_set, :orig_params, :unbound_param_pool, :tested
6
- private :params, :t, :constraints, :test_set, :orig_params, :unbound_param_pool, :tested
7
-
8
- def initialize(params, opts = {})
9
- init_params(params, (opts[:previously_tested] || []))
10
- @t = opts[:t]
11
- unless opts[:constraints].nil?
12
- @constraints = opts[:constraints].map(&:to_a)
13
- .map { |a| a.map { |b| @params[@map_to_orig_index.key(b[0])][b[1]] } }
14
- .map(&:to_set)
15
- end
16
-
17
- raise Dither::Error, 't must be >= 2' if opts[:t] < 2
18
- raise Dither::Error, 't must be <= params.length' if opts[:t] > params.length
19
- params.each do |param|
20
- raise Dither::Error, 'param length must be > 1' if param.length < 2
21
- end
22
- end
23
-
24
- def init_params(user_params, previously_tested)
25
- tmp = []
26
- @input_params = user_params
27
- user_params.each_with_index { |e, i| tmp << [i, e] }
28
- @orig_params = tmp.sort_by { |a| a[1].length }
29
- .reverse!
30
-
31
- orig_param_map = {}
32
- @map_to_orig_index = {}
33
- @orig_params.each_with_index do |e, i|
34
- @map_to_orig_index[i] = e[0]
35
- orig_param_map[e[0]] = {}
36
- end
37
-
38
- @params = []
39
- @unbound_param_pool = []
40
- orig_params.each_with_index do |e, i|
41
- @params << (0...e[1].length).map do |j|
42
- local_param = Param.new(i, j)
43
- orig_param_map[e[0]][e[1][j]] = local_param
44
- local_param
45
- end
46
- @unbound_param_pool << UnboundParam.new(i)
47
- end
48
-
49
- @tested = []
50
- previously_tested.each do |a|
51
- local_params = []
52
- a.each_with_index do |e, i|
53
- local_params << orig_param_map[i][e]
54
- end
55
- @tested << TestCase.create(params, unbound_param_pool, local_params)
56
- end
57
-
58
- params
59
- end
60
-
61
- # return nil if unable to satisfy constraints
62
- def maximize_coverage(i, test_case, pi)
63
- current_max = 0
64
- current_max_j = 0
65
- current_matches = []
66
-
67
- (0...params[i].length).each do |j|
68
- current_param = params[i][j]
69
- test_case << current_param
70
- unless violates_constraints?(test_case)
71
- matches = pi.select { |a| a.subset?(test_case) }
72
- count = matches.count
73
-
74
- if count > current_max
75
- current_max = count
76
- current_max_j = j
77
- current_matches = matches
78
- end
79
- end
80
- test_case.delete(current_param)
81
- end
82
-
83
- return nil if violates_constraints?(test_case)
84
- test_case << params[i][current_max_j]
85
-
86
- current_matches
87
- end
88
-
89
- def violates_constraints?(params)
90
- return false if constraints.nil?
91
- constraints.any? { |b| b.subset?(params) }
92
- end
93
-
94
- private
95
-
96
- def comb
97
- ranges = (0...t).to_a.inject([]) do |a, i|
98
- a << (0...params[i].length).map { |j| params[i][j] }
99
- end
100
-
101
- products = ranges[1..-1].inject(ranges[0]) do |a, b|
102
- a = a.product(b)
103
- end
104
-
105
- result = products.map(&:flatten)
106
- .map { |a| TestCase.create(params, unbound_param_pool, a) }
107
- result.reject { |a| tested?(a) }
108
- end
109
-
110
- def comb_i(param_i)
111
- values = (0...param_i).to_a.combination((t-1)).to_a
112
- values.each do |a|
113
- a << param_i
114
- end
115
- result = []
116
- values.each do |a|
117
- result += a[1..-1]
118
- .inject((0...params[a[0]].length).map { |b| params[a[0]][b] }) { |p, i| p.product((0...params[i].length).to_a.map { |c| params[i][c] }) }
119
- .map(&:flatten)
120
- .map { |b| TestCase.create(params, unbound_param_pool, b) }
121
- end
122
- result.reject { |a| tested?(a) }.to_set
123
- end
124
-
125
- def tested?(test_case)
126
- @tested.any? { |a| test_case.subset?(a) }
127
- end
128
-
129
- def fill_unbound(data)
130
- arr = Array.new(params.length)
131
- data.each do |param|
132
- unless param.unbound?
133
- i = @map_to_orig_index[param.i]
134
- arr[i] = @input_params[i][param.j]
135
- end
136
- end
137
-
138
- arr.each_with_index do |e, i|
139
- next unless e.nil?
140
-
141
- orig_param = @input_params[i]
142
- (0...orig_param.length).each do |j|
143
- data << params[@map_to_orig_index.key(i)][j]
144
- if violates_constraints?(data)
145
- data.delete(params[@map_to_orig_index.key(i)][j])
146
- next
147
- else
148
- arr[i] = orig_param[j]
149
- break
150
- end
151
- end
152
- return nil if arr[i].nil?
153
- end
154
-
155
- return nil if violates_constraints?(data)
156
- return nil if tested?(data)
157
-
158
- arr
159
- end
160
- end # IPOGHelper
161
- end # Dither