HEBench
hebench_simple_set_intersection.cpp
Go to the documentation of this file.
1 
2 // Copyright (C) 2021 Intel Corporation
3 // SPDX-License-Identifier: Apache-2.0
4 
5 #include <cassert>
6 #include <iostream>
7 #include <numeric>
8 #include <sstream>
9 #include <utility>
10 
11 #include "../include/hebench_simple_set_intersection.h"
12 #include "hebench/modules/general/include/hebench_math_utils.h"
13 
14 namespace hebench {
15 namespace TestHarness {
16 namespace SimpleSetIntersection {
17 
18 //------------------------------------
19 // class BenchmarkDescriptionCategory
20 //------------------------------------
21 
27  };
28 
29 std::array<std::uint64_t, BenchmarkDescriptorCategory::WorkloadParameterCount> BenchmarkDescriptorCategory::fetchSetSize(const std::vector<hebench::APIBridge::WorkloadParam> &w_params)
30 {
31  assert(WorkloadParameterCount == 3);
32  assert(OpParameterCount == 2);
33  assert(OpResultCount == 1);
34 
35  std::array<std::uint64_t, WorkloadParameterCount> retval;
36 
37  if (w_params.size() < WorkloadParameterCount)
38  {
39  std::stringstream ss;
40  ss << "Insufficient workload parameters in 'w_params'. Expected " << WorkloadParameterCount
41  << ", but " << w_params.size() << "received.";
42  throw std::invalid_argument(IL_LOG_MSG_CLASS(ss.str()));
43  } // end if
44 
45  // validate parameters
46  for (std::size_t i = 0; i < WorkloadParameterCount; ++i)
47  if (w_params[i].data_type != WorkloadParameterType[i])
48  {
49  std::stringstream ss;
50  ss << "Invalid type for workload parameter " << i
51  << ". Expected type ID " << WorkloadParameterType[i] << ", but " << w_params[i].data_type << " received.";
52  throw std::invalid_argument(IL_LOG_MSG_CLASS(ss.str()));
53  } // end if
54  else if (w_params[i].u_param <= 0)
55  {
56  std::stringstream ss;
57  ss << "Invalid number of elements for vector in workload parameter " << i
58  << ". Expected positive integer, but " << w_params[i].u_param << " received.";
59  throw std::invalid_argument(IL_LOG_MSG_CLASS(ss.str()));
60  } // end else if
61 
62  retval.at(0) = w_params.at(0).u_param; // n
63  retval.at(1) = w_params.at(1).u_param; // m
64  retval.at(2) = w_params.at(2).u_param; // k
65 
66  return retval;
67 }
68 
70  const Engine &engine,
71  const BenchmarkDescription::Backend &backend_desc,
72  const BenchmarkDescription::Configuration &config) const
73 {
74  (void)engine;
75  std::stringstream ss;
76 
77  output.concrete_descriptor = backend_desc.descriptor;
79  backend_desc.descriptor,
80  config,
82 
83  // workload name
84 
85  auto sets_size = fetchSetSize(config.w_params);
86  ss << BaseWorkloadName
87  << " |X| -> " << sets_size[0] << ", "
88  << "|Y| -> " << sets_size[1] << ", "
89  << "k -> " << sets_size[2];
90 
91  output.workload_name = ss.str();
94 }
95 
97  const std::vector<hebench::APIBridge::WorkloadParam> &w_params) const
98 {
99  bool retval = false;
100 
101  // return true if benchmark is supported
103  {
104  try
105  {
106  fetchSetSize(w_params);
107  retval = true;
108  }
109  catch (...)
110  {
111  // workload not supported
112  retval = false;
113  }
114  } // end if
115 
116  return retval;
117 }
118 
119 //---------------------------
120 // class DataGeneratorHelper
121 //---------------------------
122 
127 {
128 private:
129  IL_DECLARE_CLASS_NAME(SimpleSetIntersection::DataGeneratorHelper)
130 
131 public:
133  void *result, const void *x, const void *y,
134  std::uint64_t n, std::uint64_t m, std::uint64_t k);
135 
136 protected:
138 
139 private:
140  template <class T>
141  static bool isMemberOf(const T *dataset, const T *value, std::uint64_t n, std::uint64_t k)
142  {
143  bool retval = false;
144  for (size_t i = 0; !retval && i < n; ++i)
145  {
146  std::uint64_t members = 0;
147  bool flag = true;
148  for (size_t j = 0; flag && j < k; ++j)
149  {
150  flag = dataset[(i * k) + j] == value[j];
151  if (flag)
152  {
153  ++members;
154  }
155  }
156  retval = members == k;
157  }
158  return retval;
159  }
160 
161  template <class T>
162  static void mySetIntersection(T *result, const T *dataset_X, const T *dataset_Y, std::uint64_t n, std::uint64_t m, std::uint64_t k)
163  {
164  size_t idx_result = 0;
165 
166  // initialize result with all zeros
167  std::fill(result, result + m * k, static_cast<T>(0));
168 
169  for (size_t idx_x = 0; idx_x < n; ++idx_x)
170  {
171  if (isMemberOf(dataset_Y, dataset_X + (idx_x * k), m, k))
172  {
173  // check for duplicates
174  if (!isMemberOf(result, dataset_X + (idx_x * k), m, k))
175  {
176  std::copy(dataset_X + (idx_x * k),
177  dataset_X + (idx_x * k) + k,
178  result + (idx_result * k));
179  ++idx_result;
180  } // end if
181  }
182  }
183  }
184 
185  template <class T>
186  static void vectorSimpleSetIntersection(T *result, const T *x, const T *y, std::uint64_t n, std::uint64_t m, std::uint64_t k)
187  {
188  if (!x)
189  throw std::invalid_argument(IL_LOG_MSG_CLASS("Invalid null `x`"));
190  if (!y)
191  throw std::invalid_argument(IL_LOG_MSG_CLASS("Invalid null `y`"));
192  if (k == 0)
193  throw std::invalid_argument(IL_LOG_MSG_CLASS("Invalid null `k`"));
194 
195  if (n > m)
196  {
197  mySetIntersection(result, x, y, n, m, k);
198  }
199  else
200  {
201  mySetIntersection(result, y, x, m, n, k);
202  }
203  }
204 };
205 
207  void *result, const void *x, const void *y,
208  std::uint64_t n, std::uint64_t m, std::uint64_t k)
209 {
210  if (!result)
211  {
212  throw std::invalid_argument(IL_LOG_MSG_CLASS("Invalid null 'p_result'."));
213  }
214 
215  switch (data_type)
216  {
218  vectorSimpleSetIntersection<std::int32_t>(reinterpret_cast<std::int32_t *>(result),
219  reinterpret_cast<const std::int32_t *>(x), reinterpret_cast<const std::int32_t *>(y),
220  n, m, k);
221  break;
222 
224  vectorSimpleSetIntersection<std::int64_t>(reinterpret_cast<std::int64_t *>(result),
225  reinterpret_cast<const std::int64_t *>(x), reinterpret_cast<const std::int64_t *>(y),
226  n, m, k);
227  break;
228 
230  vectorSimpleSetIntersection<float>(reinterpret_cast<float *>(result),
231  reinterpret_cast<const float *>(x), reinterpret_cast<const float *>(y),
232  n, m, k);
233  break;
234 
236  vectorSimpleSetIntersection<double>(reinterpret_cast<double *>(result),
237  reinterpret_cast<const double *>(x), reinterpret_cast<const double *>(y),
238  n, m, k);
239  break;
240 
241  default:
242  throw std::invalid_argument(IL_LOG_MSG_CLASS("Unknown data type."));
243  break;
244  } // end switch
245 }
246 
247 //------------------
248 // class DataLoader
249 //------------------
250 
251 DataLoader::Ptr DataLoader::create(std::uint64_t set_size_x,
252  std::uint64_t set_size_y,
253  std::uint64_t batch_size_x,
254  std::uint64_t batch_size_y,
255  std::uint64_t element_size_k,
257 {
259  retval->init(set_size_x, set_size_y, batch_size_x, batch_size_y, element_size_k, data_type);
260  return retval;
261 }
262 
263 DataLoader::Ptr DataLoader::create(std::uint64_t set_size_x,
264  std::uint64_t set_size_y,
265  std::uint64_t batch_size_x,
266  std::uint64_t batch_size_y,
267  std::uint64_t element_size_k,
269  const std::string &dataset_filename)
270 {
272  retval->init(set_size_x, set_size_y, batch_size_x, batch_size_y, element_size_k, data_type, dataset_filename);
273  return retval;
274 }
275 
276 DataLoader::DataLoader() :
277  m_set_size_x(0), m_set_size_y(0), m_element_size_k(1)
278 {
279 }
280 
281 void DataLoader::init(std::uint64_t set_size_x,
282  std::uint64_t set_size_y,
283  std::uint64_t batch_size_x,
284  std::uint64_t batch_size_y,
285  std::uint64_t element_size_k,
287 {
288  // Load/generate and initialize the data for vectors to be applied for simple set intersection:
289  // Z = Intersect(X, Y)
290 
291  // number of samples in each input parameter and output
292  std::size_t batch_sizes[InputDim0 + OutputDim0] = {
293  batch_size_x,
294  batch_size_y,
295  (batch_size_x * batch_size_y)
296  };
297 
298  m_set_size_x = set_size_x;
299  m_set_size_y = set_size_y;
300  m_element_size_k = element_size_k;
301 
302  // compute buffer size in bytes for each set (vector)
303  std::uint64_t sample_set_sizes[InputDim0 + OutputDim0] = {
304  set_size_x * element_size_k,
305  set_size_y * element_size_k,
306  std::min(set_size_x, set_size_y) * element_size_k // Assuming there is a set that contains the other
307  };
308 
309  // initialize data packs and allocate memory
310  PartialDataLoader::init(data_type,
311  InputDim0, batch_sizes, sample_set_sizes,
312  OutputDim0, sample_set_sizes + InputDim0,
313  true);
314 
315  // at this point all NativeDataBuffers have been allocated and pointed to the correct locations
316 
317  // fill up each set (vector) data
318 
319  constexpr std::size_t Param_SetX = 0;
320  constexpr std::size_t Param_SetY = 1;
321 
322  // generate setX data
323  for (std::uint64_t sample_i = 0; sample_i < batch_sizes[Param_SetX]; ++sample_i)
324  {
326  getParameterData(Param_SetX).p_buffers[sample_i].p,
327  sample_set_sizes[Param_SetX],
328  -16384.0, 16384.0);
329  } // end for
330 
331  // generate setY data
332 
333  for (std::uint64_t sample_i = 0; sample_i < batch_sizes[Param_SetY]; ++sample_i)
334  {
335  std::vector<std::uint64_t> indices_y = DataGeneratorHelper::generateRandomIntersectionIndicesU(set_size_y);
336  std::vector<std::uint64_t> indices_x;
337  std::vector<std::uint64_t>::iterator it_indices_y;
338  if (!indices_y.empty())
339  {
340  // if indices_y is empty, there's no point to execute the following statements
341  std::sort(indices_y.begin(), indices_y.end());
342  // This will randomly select indices in X, same amount than the ones in indices_y.
343  indices_x = DataGeneratorHelper::generateRandomIntersectionIndicesU(set_size_x, indices_y.size());
344  it_indices_y = indices_y.begin();
345  }
346  // find which sample from X to copy
347  std::uint64_t sample_from_X = DataGeneratorHelper::generateRandomIntU(0, getParameterData(Param_SetX).buffer_count - 1);
348  std::uint8_t *p_setX = reinterpret_cast<std::uint8_t *>(getParameterData(Param_SetX).p_buffers[sample_from_X].p);
349  std::uint8_t *p_setY = reinterpret_cast<std::uint8_t *>(getParameterData(Param_SetY).p_buffers[sample_i].p);
350  for (std::uint64_t item_y = 0; item_y < set_size_y; ++item_y)
351  {
352  std::uint8_t *p_setY_item = p_setY + item_y * element_size_k * sizeOf(data_type);
353  if (!indices_x.empty() && item_y == *it_indices_y)
354  {
355  // we found a common item to set
356  std::uint8_t *p_setX_item = p_setX + indices_x.back() * element_size_k * sizeOf(data_type);
357  std::copy(p_setX_item, p_setX_item + element_size_k * sizeOf(data_type),
358  p_setY_item);
359  indices_x.pop_back();
360  ++it_indices_y;
361  } // end if
362  else
363  {
364  // generate (possibly unique item)
366  p_setY_item,
367  element_size_k,
368  -16384.0, 16384.0);
369  } // end else
370  } // end for
371  } // end for
372 
373  for (std::uint64_t sampleX_i = 0; sampleX_i < batch_sizes[Param_SetX]; ++sampleX_i)
374  for (std::uint64_t sampleY_i = 0; sampleY_i < batch_sizes[Param_SetY]; ++sampleY_i)
375  {
376  // find the index for the result buffer based on the input indices
377  std::uint64_t ppi[] = { sampleX_i, sampleY_i };
378  std::uint64_t z_i = getResultIndex(ppi);
379 
380  // generate the data
382  getResultData(0).p_buffers[z_i].p, // Z
383  getParameterData(0).p_buffers[sampleX_i].p, // X
384  getParameterData(1).p_buffers[sampleY_i].p, // Y
385  set_size_x, set_size_y, element_size_k);
386  } // end for
387 
388  // all data has been generated at this point
389 }
390 
391 void DataLoader::init(std::uint64_t set_size_x,
392  std::uint64_t set_size_y,
393  std::uint64_t batch_size_x,
394  std::uint64_t batch_size_y,
395  std::uint64_t element_size_k,
397  const std::string &dataset_filename)
398 {
399  // Load/generate and initialize the data for simple set intersection:
400  // Z = X ∩ Y
401 
402  // number of samples in each input parameter and output
403  std::size_t batch_sizes[InputDim0 + OutputDim0] = {
404  batch_size_x,
405  batch_size_y,
406  (batch_size_x * batch_size_y)
407  };
408 
409  m_set_size_x = set_size_x;
410  m_set_size_y = set_size_y;
411  m_element_size_k = element_size_k;
412 
413  // compute buffer size in bytes for each set (vector)
414  std::uint64_t sample_set_sizes[InputDim0 + OutputDim0] = {
415  set_size_x * element_size_k,
416  set_size_y * element_size_k,
417  std::min(set_size_x, set_size_y) * element_size_k // Assuming there is a set that contains the other
418  };
419 
420  // allocate memory for each set (vector) buffer
421  PartialDataLoader::init(dataset_filename, data_type,
422  InputDim0, batch_sizes, sample_set_sizes,
423  OutputDim0, sample_set_sizes + InputDim0);
424 
425  // at this point all NativeDataBuffers have been allocated and pointed to the correct locations
426  // and buffers loaded with data from dataset_filename
427 }
428 
429 void DataLoader::computeResult(std::vector<hebench::APIBridge::NativeDataBuffer *> &result,
430  const std::uint64_t *param_data_pack_indices,
432 {
433  // as protected method, parameters should be valid when called
434 
435  // generate the output
437  result.front()->p, // Z
438  getParameterData(0).p_buffers[param_data_pack_indices[0]].p, // X
439  getParameterData(1).p_buffers[param_data_pack_indices[1]].p, // Y
440  m_set_size_x, m_set_size_y, m_element_size_k);
441 }
442 
444  const std::uint64_t *param_data_pack_indices,
445  const std::vector<hebench::APIBridge::NativeDataBuffer *> &outputs,
446  std::uint64_t k_count,
448 {
449  static constexpr const std::size_t MaxErrorPrint = 10;
450  bool retval = true;
451  //std::vector<std::uint64_t> is_valid;
452  // true => truth has extra element (not in output)
453  // false => output has extra element (not in truth)
454  std::vector<std::pair<bool, std::uint64_t>> is_valid;
455 
456  // extract the pointers to the actual results
457 
458  if (outputs.size() != dataset->getResultCount())
459  {
460  throw std::invalid_argument(IL_LOG_MSG("Invalid number of outputs: 'outputs'."));
461  }
462 
463  IDataLoader::ResultDataPtr ptr_truths = dataset->getResultFor(param_data_pack_indices);
464  const std::vector<const hebench::APIBridge::NativeDataBuffer *> &truths =
465  ptr_truths->result;
466 
467  // There's at least 1 element that requires processing.
468  if (!truths.empty() && !outputs.empty() && truths.front())
469  {
470  std::size_t index = 0;
471  for (index = 0; retval && index < truths.size(); ++index)
472  {
473  // in case outputs.size() < truths.size() an exception can be triggered
474  try
475  {
476  if (!outputs.at(index))
477  {
478  throw std::invalid_argument(IL_LOG_MSG("Unexpected null output component in: 'outputs[" + std::to_string(index) + "]'."));
479  }
480  }
481  catch (const std::out_of_range &out_of_range)
482  {
483  throw std::invalid_argument(IL_LOG_MSG("Unexpected out of range index output component in: 'outputs[" + std::to_string(index) + "]'."));
484  }
485 
486  if (outputs.at(index)->size < truths.at(index)->size)
487  {
488  throw std::invalid_argument(IL_LOG_MSG("Buffer in outputs is not large enough to contain the expected output: 'outputs[" + std::to_string(index) + "]'."));
489  }
490 
491  void *p_truth = truths.at(index)->p;
492  void *p_output = outputs.at(index)->p; // single output
493 
494  // m is not required since both collections have the same size
495  std::uint64_t n = truths.at(index)->size / IDataLoader::sizeOf(data_type) / k_count;
496 
497  // validate the results
498  switch (data_type)
499  {
501  is_valid = almostEqualSet(reinterpret_cast<const std::int32_t *>(p_truth),
502  reinterpret_cast<const std::int32_t *>(p_output),
503  n, n, k_count,
504  0.01);
505  break;
506 
508  is_valid = almostEqualSet(reinterpret_cast<const std::int64_t *>(p_truth),
509  reinterpret_cast<const std::int64_t *>(p_output),
510  n, n, k_count,
511  0.01);
512  break;
513 
515  is_valid = almostEqualSet(reinterpret_cast<const float *>(p_truth),
516  reinterpret_cast<const float *>(p_output),
517  n, n, k_count,
518  0.01);
519  break;
520 
522  is_valid = almostEqualSet(reinterpret_cast<const double *>(p_truth),
523  reinterpret_cast<const double *>(p_output),
524  n, n, k_count,
525  0.01);
526  break;
527 
528  default:
529  retval = false;
530  break;
531  } // end switch
532 
533  // In case retval is set to false, it will break the for loop
534  retval = retval && is_valid.empty();
535  } // end for
536 
537  if (!retval)
538  {
539  std::stringstream ss;
540  ss << "Result component, " << (index - 1) << std::endl
541  << "Elements mismatched, " << is_valid.size() << std::endl
542  << "Failed indices, ";
543  for (std::size_t i = 0; i < is_valid.size() && i < MaxErrorPrint; ++i)
544  {
545  ss << "(" << (is_valid[i].first ? "ground truth" : "output") << "; " << is_valid[i].second * k_count << ".." << is_valid[i].second * k_count + k_count - 1 << ")";
546  if (i + 1 < is_valid.size() && i + 1 < MaxErrorPrint)
547  {
548  ss << ", ";
549  }
550  } // end for
551  if (is_valid.size() > MaxErrorPrint)
552  {
553  ss << ", ...";
554  }
555  throw std::runtime_error(ss.str());
556  } // end if
557  } // end if
558 
559  return retval;
560 }
561 
562 } // namespace SimpleSetIntersection
563 } // namespace TestHarness
564 } // namespace hebench
const hebench::APIBridge::BenchmarkDescriptor & descriptor
Benchmark backend descriptor, as retrieved by backend, corresponding to the registration handle h_des...
std::vector< hebench::APIBridge::WorkloadParam > w_params
Set of arguments for workload parameters.
Static helper class to generate vector data for all supported data types.
static std::vector< std::uint64_t > generateRandomIntersectionIndicesU(std::uint64_t elem_count, std::uint64_t indices_count=0)
Generates uniform random amount of indices.
static std::uint64_t generateRandomIntU(std::uint64_t min_val, std::uint64_t max_val)
static void generateRandomVectorU(hebench::APIBridge::DataType data_type, void *result, std::uint64_t elem_count, double min_val, double max_val)
Generates uniform random data of the specified type.
static std::size_t sizeOf(hebench::APIBridge::DataType data_type)
std::shared_ptr< ResultData > ResultDataPtr
std::shared_ptr< IDataLoader > Ptr
static void completeCategoryParams(hebench::APIBridge::BenchmarkDescriptor &out_descriptor, const hebench::APIBridge::BenchmarkDescriptor &in_descriptor, const BenchmarkDescription::Configuration &config, bool force_config)
Completes common elements of category parameters in a descriptor using the specified configuration.
static bool getForceConfigValues()
Specifies whether frontend will override backend descriptors using configuration data or not.
std::size_t operation_params_count
Number of parameters for the represented workload operation.
std::string workload_name
Human-readable friendly name for the represented workload to be used for its description on the repor...
hebench::APIBridge::BenchmarkDescriptor concrete_descriptor
Benchmark descriptor completed with concrete values assigned to configurable fields.
std::string workload_base_name
Human-readable friendly name for the represented workload to be used for its description on the repor...
Bundles values that need to be filled by a workload during completeWorkloadDescription().
const hebench::APIBridge::DataPack & getResultData(std::uint64_t param_position) const override
Data pack corresponding to the specified component of the result.
const hebench::APIBridge::DataPack & getParameterData(std::uint64_t param_position) const override
Data pack for specified operation parameter (operand).
void init(hebench::APIBridge::DataType data_type, std::size_t input_dim, const std::size_t *input_sample_count_per_dim, const std::uint64_t *input_count_per_dim, std::size_t output_dim, const std::uint64_t *output_count_per_dim, bool allocate_output)
Initializes dimensions of inputs and outputs. No allocation is performed.
std::uint64_t getResultIndex(const std::uint64_t *param_data_pack_indices) const override
Computes the index of the result NativeDataBuffer given the indices of the input data.
void completeWorkloadDescription(WorkloadDescriptionOutput &output, const Engine &engine, const BenchmarkDescription::Backend &backend_desc, const BenchmarkDescription::Configuration &config) const override
Completes the description for the matched benchmark.
static std::array< std::uint64_t, BenchmarkDescriptorCategory::WorkloadParameterCount > fetchSetSize(const std::vector< hebench::APIBridge::WorkloadParam > &w_params)
static hebench::APIBridge::WorkloadParamType::WorkloadParamType WorkloadParameterType[WorkloadParameterCount]
bool matchBenchmarkDescriptor(const hebench::APIBridge::BenchmarkDescriptor &bench_desc, const std::vector< hebench::APIBridge::WorkloadParam > &w_params) const override
Determines if the represented benchmark can perform the workload described by a specified HEBench ben...
Static helper class to generate vector data for all supported data types.
static void vectorSimpleSetIntersection(hebench::APIBridge::DataType data_type, void *result, const void *x, const void *y, std::uint64_t n, std::uint64_t m, std::uint64_t k)
void computeResult(std::vector< hebench::APIBridge::NativeDataBuffer * > &result, const std::uint64_t *param_data_pack_indices, hebench::APIBridge::DataType data_type) override
Computes result of the operation on the input data given the of the input sample.
static DataLoader::Ptr create(std::uint64_t set_size_x, std::uint64_t set_size_y, std::uint64_t batch_size_x, std::uint64_t batch_size_y, std::uint64_t element_size_k, hebench::APIBridge::DataType data_type)
WorkloadParamType
Defines the possible data types for a workload flexible parameter.
Definition: types.h:303
@ Float64
64 bits IEEE 754 standard floating point real numbers.
Definition: types.h:306
@ Int64
64 bits signed integers.
Definition: types.h:304
@ UInt64
64 bits unsigned integers.
Definition: types.h:305
DataType
Defines data types for a workload.
Definition: types.h:379
@ Float32
32 bits IEEE 754 standard floating point real numbers.
Definition: types.h:382
@ Int32
32 bits signed integers.
Definition: types.h:380
void * p
Pointer to underlying data.
Definition: types.h:558
Workload workload
Workload for the benchmark.
Definition: types.h:529
NativeDataBuffer * p_buffers
Array of data buffers for parameter.
Definition: types.h:612
@ SimpleSetIntersection
Definition: types.h:268
Defines a benchmark test.
Definition: types.h:527
std::string to_string(const std::string_view &s)
bool validateResult(IDataLoader::Ptr dataset, const std::uint64_t *param_data_pack_indices, const std::vector< hebench::APIBridge::NativeDataBuffer * > &p_outputs, std::uint64_t k_count, hebench::APIBridge::DataType data_type)
std::enable_if< std::is_integral< T >::value||std::is_floating_point< T >::value, std::vector< std::pair< bool, std::uint64_t > > >::type almostEqualSet(const T *X, const T *Y, std::uint64_t n, std::uint64_t m, std::uint64_t k, double pct=0.05)
Finds whether values in two arrays are within a certain percentage of each other.