pnmatrix 2.0.0 → 2.0.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c61ebac7b89f8d0fd43e09fdbe74aa0ef1564232c1f56224837cf8e1dd3ea7cc
4
- data.tar.gz: 19c1789f16a5c87a91b2b0de44dbfa39bc024a117f166a43bd466528b7fe24c7
3
+ metadata.gz: a13074d0b3e5e2bbee2a456f42fe5d5d7a4b4e581e2ae42e149d74f4a1452211
4
+ data.tar.gz: 917446ac40ebe45b13a62c6c0e1e927227beee199a411e2c0cc1fa83f5e9f1bc
5
5
  SHA512:
6
- metadata.gz: a53e78fa91ce0d8dbc30a5a0d9ad035d768238f3c9fba3de3f7eb1e181e4f1756c815db600bfc40e608c26aa6810083b69b4442a4a338904c7581209db9d0092
7
- data.tar.gz: 2ef44f7c3f1079411c29c189d7c988c6432521f08d3291bcfbf9e555b400ac685a09673db8d07175a281a7973b774ebac4141cb176f004c38e2557b791486373
6
+ metadata.gz: 697596b71a0d302a785d4fe503676fe89392b60e84354fba5151b873f2bbf570e11f6086972c30bdf610eb9a2621387b5bf469c82280e8586888241773a3668f
7
+ data.tar.gz: 2a42bf5f5e4c97748e6d44aa631086e94c18bdb0b6f56503ce236d97a57cb8da0a272a016686b88d656f67f86d82124f9bef5bb7fbbfe39b91deefd6c657810d
@@ -376,6 +376,14 @@ NM_DEF_STRUCT_POST(NM_GC_HOLDER); // };
376
376
  // enum for a list of possible storage types.
377
377
  #define NM_STYPE(val) (NM_STRUCT(val)->stype)
378
378
 
379
+ /*
380
+ * Object dtype storage keeps Ruby VALUEs in malloc-backed arrays/lists, not in
381
+ * Ruby object fields. After mutating that native storage, make the wrapper
382
+ * object write-barrier-unprotected so minor GC runs nm_mark and sees newly
383
+ * assigned VALUEs before they can be collected.
384
+ */
385
+ #define NM_OBJECT_STORAGE_WB_UNPROTECT(val) RB_OBJ_WB_UNPROTECT(val)
386
+
379
387
  // Get the shape of the ith dimension (int)
380
388
  #define NM_SHAPE(val,i) (NM_STORAGE(val)->shape[(i)])
381
389
 
@@ -412,8 +412,14 @@ static VALUE nm_alloc(VALUE klass) {
412
412
  mat->stype = nm::DENSE_STORE;
413
413
  mat->storage = NULL;
414
414
 
415
- // DO NOT MARK This STRUCT. It has no storage allocated, and no stype, so mark will do an invalid something.
416
- return Data_Wrap_Struct(klass, NULL, nm_delete, mat);
415
+ /*
416
+ * Newly allocated NMatrix wrappers do not have storage yet, but nm_mark is
417
+ * explicitly null-storage safe. Installing it here is required for object
418
+ * dtype matrices after initialization: otherwise Ruby never asks the matrix
419
+ * to mark VALUEs stored in native dense/list/Yale memory, so a later GC can
420
+ * reclaim objects that were assigned into the matrix.
421
+ */
422
+ return Data_Wrap_Struct(klass, nm_mark, nm_delete, mat);
417
423
  }
418
424
 
419
425
  /*
@@ -102,17 +102,21 @@ namespace nm { namespace dense_storage {
102
102
  * Recursive function, sets multiple values in a matrix from a single source value. Same basic pattern as slice_copy.
103
103
  */
104
104
  template <typename D>
105
- static void slice_set(DENSE_STORAGE* dest, size_t* lengths, size_t pdest, size_t rank, D* const v, size_t v_size, size_t& v_offset) {
105
+ static void slice_set(VALUE owner, DENSE_STORAGE* dest, size_t* lengths, size_t pdest, size_t rank, D* const v, size_t v_size, size_t& v_offset) {
106
106
  if (dest->dim - rank > 1) {
107
107
  for (size_t i = 0; i < lengths[rank]; ++i) {
108
- slice_set<D>(dest, lengths, pdest + dest->stride[rank] * i, rank + 1, v, v_size, v_offset);
108
+ slice_set<D>(owner, dest, lengths, pdest + dest->stride[rank] * i, rank + 1, v, v_size, v_offset);
109
109
  }
110
110
  } else {
111
111
  for (size_t p = 0; p < lengths[rank]; ++p, ++v_offset) {
112
112
  if (v_offset >= v_size) v_offset %= v_size;
113
113
 
114
114
  D* elem = reinterpret_cast<D*>(dest->elements);
115
- elem[p + pdest] = v[v_offset];
115
+ if (dest->dtype == nm::RUBYOBJ) {
116
+ RB_OBJ_WRITE(owner, reinterpret_cast<VALUE*>(elem) + p + pdest, reinterpret_cast<VALUE*>(v)[v_offset]);
117
+ } else {
118
+ elem[p + pdest] = v[v_offset];
119
+ }
116
120
  }
117
121
  }
118
122
  }
@@ -127,6 +131,9 @@ namespace nm { namespace dense_storage {
127
131
  NM_CONSERVATIVE(nm_register_value(&right));
128
132
 
129
133
  DENSE_STORAGE* s = NM_STORAGE_DENSE(left);
134
+ if (s->dtype == nm::RUBYOBJ) {
135
+ NM_OBJECT_STORAGE_WB_UNPROTECT(left);
136
+ }
130
137
 
131
138
  std::pair<NMATRIX*,bool> nm_and_free =
132
139
  interpret_arg_as_dense_nmatrix(right, s->dtype);
@@ -158,10 +165,15 @@ namespace nm { namespace dense_storage {
158
165
  }
159
166
 
160
167
  if (slice->single) {
161
- reinterpret_cast<D*>(s->elements)[nm_dense_storage_pos(s, slice->coords)] = *v;
168
+ size_t pos = nm_dense_storage_pos(s, slice->coords);
169
+ if (s->dtype == nm::RUBYOBJ) {
170
+ RB_OBJ_WRITE(left, reinterpret_cast<VALUE*>(s->elements) + pos, *reinterpret_cast<VALUE*>(v));
171
+ } else {
172
+ reinterpret_cast<D*>(s->elements)[pos] = *v;
173
+ }
162
174
  } else {
163
175
  size_t v_offset = 0;
164
- slice_set(s, slice->lengths, nm_dense_storage_pos(s, slice->coords), 0, v, v_size, v_offset);
176
+ slice_set(left, s, slice->lengths, nm_dense_storage_pos(s, slice->coords), 0, v, v_size, v_offset);
165
177
  }
166
178
 
167
179
  // Only free v if it was allocated in this function.
@@ -311,14 +323,25 @@ void nm_dense_storage_mark(STORAGE* storage_base) {
311
323
  DENSE_STORAGE* storage = (DENSE_STORAGE*)storage_base;
312
324
 
313
325
  if (storage && storage->dtype == nm::RUBYOBJ) {
314
- VALUE* els = reinterpret_cast<VALUE*>(storage->elements);
315
-
316
- if (els) {
317
- rb_gc_mark_locations(els, &(els[nm_storage_count_max_elements(storage)-1]));
326
+ /*
327
+ * Dense slice references share the source storage's elements pointer and
328
+ * may start at a non-zero offset. Marking only the reference's element
329
+ * count from offset zero can miss Ruby objects that are visible through
330
+ * the reference. Mark the backing source storage so every object that a
331
+ * live reference can expose stays alive.
332
+ */
333
+ DENSE_STORAGE* src = reinterpret_cast<DENSE_STORAGE*>(storage->src);
334
+
335
+ size_t count = src ? nm_storage_count_max_elements(src) : 0;
336
+ if (src && src->elements && count > 0) {
337
+ VALUE* els = reinterpret_cast<VALUE*>(src->elements);
338
+ /*
339
+ * This storage holds exact Ruby VALUEs, not arbitrary machine words.
340
+ * Mark each slot explicitly so objects assigned into malloc-backed
341
+ * matrix memory are kept alive just like Ruby object fields would be.
342
+ */
343
+ for (size_t i = 0; i < count; ++i) rb_gc_mark(els[i]);
318
344
  }
319
- //for (size_t index = nm_storage_count_max_elements(storage); index-- > 0;) {
320
- // rb_gc_mark(els[index]);
321
- //}
322
345
  }
323
346
  }
324
347
 
@@ -538,6 +538,9 @@ void set(VALUE left, SLICE* slice, VALUE right) {
538
538
  NM_CONSERVATIVE(nm_register_value(&left));
539
539
  NM_CONSERVATIVE(nm_register_value(&right));
540
540
  LIST_STORAGE* s = NM_STORAGE_LIST(left);
541
+ if (s->dtype == nm::RUBYOBJ) {
542
+ NM_OBJECT_STORAGE_WB_UNPROTECT(left);
543
+ }
541
544
 
542
545
  std::pair<NMATRIX*,bool> nm_and_free =
543
546
  interpret_arg_as_dense_nmatrix(right, NM_DTYPE(left));
@@ -50,7 +50,18 @@ public:
50
50
  : s(reinterpret_cast<YALE_STORAGE*>(storage->src)),
51
51
  slice(storage != storage->src),
52
52
  slice_shape(storage->shape),
53
- slice_offset(storage->offset)
53
+ slice_offset(storage->offset),
54
+ owner(Qnil)
55
+ {
56
+ nm_yale_storage_register(storage->src);
57
+ }
58
+
59
+ YaleStorage(const YALE_STORAGE* storage, VALUE owner_)
60
+ : s(reinterpret_cast<YALE_STORAGE*>(storage->src)),
61
+ slice(storage != storage->src),
62
+ slice_shape(storage->shape),
63
+ slice_offset(storage->offset),
64
+ owner(owner_)
54
65
  {
55
66
  nm_yale_storage_register(storage->src);
56
67
  }
@@ -59,7 +70,8 @@ public:
59
70
  : s(reinterpret_cast<YALE_STORAGE*>(storage->src)),
60
71
  slice(storage != storage->src),
61
72
  slice_shape(storage->shape),
62
- slice_offset(storage->offset)
73
+ slice_offset(storage->offset),
74
+ owner(Qnil)
63
75
  {
64
76
  nm_yale_storage_register(reinterpret_cast<STORAGE*>(storage->src));
65
77
  }
@@ -96,6 +108,17 @@ public:
96
108
  inline const D& a(size_t p) const { return a_p()[p]; }
97
109
  inline D& a(size_t p) { return a_p()[p]; }
98
110
 
111
+ // Object matrices store Ruby VALUEs in native memory, so replacing a value must
112
+ // go through Ruby's write barrier. Without it, generational GC can collect a
113
+ // newly assigned object before the matrix is marked again.
114
+ inline void write_a(size_t p, const D& val) {
115
+ if (dtype() == nm::RUBYOBJ && owner != Qnil) {
116
+ RB_OBJ_WRITE(owner, reinterpret_cast<VALUE*>(s->a) + p, reinterpret_cast<const VALUE*>(&val)[0]);
117
+ } else {
118
+ a(p) = val;
119
+ }
120
+ }
121
+
99
122
  bool real_row_empty(size_t i) const { return ija(i+1) - ija(i) == 0 ? true : false; }
100
123
 
101
124
  inline size_t* shape_p() const { return slice_shape; }
@@ -467,7 +490,7 @@ public:
467
490
  * A pseudo-insert operation, since the diagonal portion of the A array is constant size.
468
491
  */
469
492
  stored_diagonal_iterator insert(stored_diagonal_iterator position, const D& val) {
470
- *position = val;
493
+ write_a(position.p(), val);
471
494
  return position;
472
495
  }
473
496
 
@@ -694,24 +717,37 @@ public:
694
717
 
695
718
  E* ns_a = reinterpret_cast<E*>(ns.a);
696
719
  size_t sz = shape(0) + 1; // current used size of ns
697
- nm_yale_storage_register(&ns);
720
+ E converted = val;
721
+ if (ns.dtype == nm::RUBYOBJ) {
722
+ nm_register_value(reinterpret_cast<VALUE*>(&converted));
723
+ }
698
724
 
699
725
  // FIXME: If diagonals line up, it's probably faster to do this with stored diagonal and stored non-diagonal iterators
700
726
  for (const_row_iterator it = cribegin(); it != criend(); ++it) {
701
727
  for (auto jt = it.begin(); !jt.end(); ++jt) {
702
728
  if (it.i() == jt.j()) {
703
- if (Yield) ns_a[it.i()] = rb_yield(~jt);
704
- else ns_a[it.i()] = static_cast<E>(*jt);
729
+ if (Yield) converted = E(rb_yield(~jt));
730
+ else converted = static_cast<E>(*jt);
731
+ if (ns.dtype == nm::RUBYOBJ) {
732
+ nm_register_value(reinterpret_cast<VALUE*>(&converted));
733
+ }
734
+ ns_a[it.i()] = converted;
705
735
  } else if (*jt != const_default_obj()) {
706
- if (Yield) ns_a[sz] = rb_yield(~jt);
707
- else ns_a[sz] = static_cast<E>(*jt);
736
+ if (Yield) converted = E(rb_yield(~jt));
737
+ else converted = static_cast<E>(*jt);
738
+ if (ns.dtype == nm::RUBYOBJ) {
739
+ nm_register_value(reinterpret_cast<VALUE*>(&converted));
740
+ }
741
+ ns_a[sz] = converted;
708
742
  ns.ija[sz] = jt.j();
709
743
  ++sz;
710
744
  }
711
745
  }
712
746
  ns.ija[it.i()+1] = sz;
713
747
  }
714
- nm_yale_storage_unregister(&ns);
748
+ if (ns.dtype == nm::RUBYOBJ) {
749
+ nm_unregister_value(reinterpret_cast<VALUE*>(&converted));
750
+ }
715
751
 
716
752
  //ns.ija[shape(0)] = sz; // indicate end of last row
717
753
  ns.ndnz = sz - shape(0) - 1; // update ndnz count
@@ -751,14 +787,24 @@ public:
751
787
 
752
788
  E* la = reinterpret_cast<E*>(lhs->a);
753
789
 
754
- nm_yale_storage_register(lhs);
790
+ E converted;
791
+ if (lhs->dtype == nm::RUBYOBJ) {
792
+ nm_register_value(reinterpret_cast<VALUE*>(&converted));
793
+ }
755
794
  for (size_t m = 0; m < size(); ++m) {
756
795
  if (Yield) {
757
- la[m] = rb_yield(nm::yale_storage::nm_rb_dereference(a(m)));
758
- }
759
- else la[m] = static_cast<E>(a(m));
796
+ converted = E(rb_yield(nm::yale_storage::nm_rb_dereference(a(m))));
797
+ } else {
798
+ converted = static_cast<E>(a(m));
799
+ }
800
+ if (lhs->dtype == nm::RUBYOBJ) {
801
+ nm_register_value(reinterpret_cast<VALUE*>(&converted));
802
+ }
803
+ la[m] = converted;
804
+ }
805
+ if (lhs->dtype == nm::RUBYOBJ) {
806
+ nm_unregister_value(reinterpret_cast<VALUE*>(&converted));
760
807
  }
761
- nm_yale_storage_unregister(lhs);
762
808
 
763
809
  }
764
810
 
@@ -995,10 +1041,18 @@ protected:
995
1041
  if (v_offset >= v_size) v_offset %= v_size;
996
1042
 
997
1043
  if (j + real_j == i + real_i) { // modify diagonal
998
- new_a[real_i + i] = v[v_offset];
1044
+ if (s->dtype == nm::RUBYOBJ && owner != Qnil) {
1045
+ RB_OBJ_WRITE(owner, reinterpret_cast<VALUE*>(new_a) + real_i + i, reinterpret_cast<const VALUE*>(v)[v_offset]);
1046
+ } else {
1047
+ new_a[real_i + i] = v[v_offset];
1048
+ }
999
1049
  } else if (v[v_offset] != const_default_obj()) {
1000
1050
  new_ija[q] = j + real_j;
1001
- new_a[q] = v[v_offset];
1051
+ if (s->dtype == nm::RUBYOBJ && owner != Qnil) {
1052
+ RB_OBJ_WRITE(owner, reinterpret_cast<VALUE*>(new_a) + q, reinterpret_cast<const VALUE*>(v)[v_offset]);
1053
+ } else {
1054
+ new_a[q] = v[v_offset];
1055
+ }
1002
1056
  ++q; // move on to next q location
1003
1057
  }
1004
1058
 
@@ -1132,6 +1186,7 @@ protected:
1132
1186
  bool slice;
1133
1187
  size_t* slice_shape;
1134
1188
  size_t* slice_offset;
1189
+ VALUE owner;
1135
1190
  };
1136
1191
 
1137
1192
  } // end of nm namespace
@@ -303,7 +303,7 @@ public:
303
303
  while (!position.end() && position.j() < jj) ++position; // position is just a hint. (This loop ideally only has to happen once.)
304
304
 
305
305
  if (!position.end() && position.j() == jj) {
306
- *position = val; // replace existing
306
+ y.write_a(position.p(), val); // replace existing
307
307
  } else {
308
308
 
309
309
  if (sz + 1 > y.capacity()) {
@@ -313,7 +313,7 @@ public:
313
313
  y.update_real_row_sizes_from(real_i(), 1);
314
314
  }
315
315
  ija(position.p()) = jj + y.offset(1); // set column ID
316
- a(position.p()) = val;
316
+ y.write_a(position.p(), val);
317
317
  adjust_length(1);
318
318
  }
319
319
 
@@ -331,7 +331,7 @@ public:
331
331
  */
332
332
  //template <typename = typename std::enable_if<!std::is_const<RefType>::value>::type>
333
333
  void insert(size_t j, const D& val) {
334
- if (j + y.offset(1) == real_i()) a(real_i()) = val;
334
+ if (j + y.offset(1) == real_i()) y.write_a(real_i(), val);
335
335
  else {
336
336
  row_stored_nd_iterator jt = ndfind(j);
337
337
  if (!jt.end() && jt.j() == j) {
@@ -404,10 +404,10 @@ public:
404
404
  if (v_offset >= v_size) v_offset %= v_size; // reset v position.
405
405
 
406
406
  if (jc + y.offset(1) == real_i()) {
407
- y.a(real_i()) = v[v_offset]; // modify diagonal
407
+ y.write_a(real_i(), v[v_offset]); // modify diagonal
408
408
  } else if (v[v_offset] != y.const_default_obj()) {
409
409
  y.ija(pp) = jc; // modify non-diagonal
410
- y.a(pp) = v[v_offset];
410
+ y.write_a(pp, v[v_offset]);
411
411
  ++pp;
412
412
  }
413
413
  }
@@ -369,7 +369,10 @@ YALE_STORAGE* ref(YALE_STORAGE* s, SLICE* slice) {
369
369
  template <typename DType>
370
370
  void set(VALUE left, SLICE* slice, VALUE right) {
371
371
  YALE_STORAGE* storage = NM_STORAGE_YALE(left);
372
- YaleStorage<DType> y(storage);
372
+ if (storage->dtype == nm::RUBYOBJ) {
373
+ NM_OBJECT_STORAGE_WB_UNPROTECT(left);
374
+ }
375
+ YaleStorage<DType> y(storage, left);
373
376
  y.insert(slice, right);
374
377
  }
375
378
 
@@ -1456,9 +1459,25 @@ void nm_yale_storage_mark(STORAGE* storage_base) {
1456
1459
  YALE_STORAGE* storage = (YALE_STORAGE*)storage_base;
1457
1460
 
1458
1461
  if (storage && storage->dtype == nm::RUBYOBJ) {
1459
-
1460
- VALUE* a = (VALUE*)(storage->a);
1461
- rb_gc_mark_locations(a, &(a[storage->capacity-1]));
1462
+ /*
1463
+ * Yale slice references are lightweight wrappers around another
1464
+ * YALE_STORAGE. They may have no A array or capacity of their own, but
1465
+ * Ruby objects returned through the reference still live in the source
1466
+ * storage. Mark the source's initialized A entries so GC cannot reclaim
1467
+ * those objects while the reference is alive.
1468
+ */
1469
+ YALE_STORAGE* src = reinterpret_cast<YALE_STORAGE*>(storage->src);
1470
+
1471
+ size_t size = src ? nm::yale_storage::get_size(src) : 0;
1472
+ if (src && src->a && size > 0) {
1473
+ VALUE* a = reinterpret_cast<VALUE*>(src->a);
1474
+ /*
1475
+ * Yale object storage keeps real Ruby VALUEs in the native A array.
1476
+ * Marking them exactly is important after mutation, because otherwise
1477
+ * GC may reclaim recently assigned objects before later iteration.
1478
+ */
1479
+ for (size_t i = 0; i < size; ++i) rb_gc_mark(a[i]);
1480
+ }
1462
1481
  }
1463
1482
  }
1464
1483
 
data/lib/nmatrix/mkmf.rb CHANGED
@@ -32,7 +32,7 @@ def find_newer_gplusplus #:nodoc:
32
32
  [9,8,7,6,5,4,3].each do |minor|
33
33
  ver = "4.#{minor}"
34
34
  gpp = "g++-#{ver}"
35
- result = `which #{gpp}`
35
+ result = `type #{gpp}`
36
36
  next if result.empty?
37
37
  CONFIG['CXX'] = gpp
38
38
  puts ver
@@ -727,7 +727,7 @@ class NMatrix
727
727
  end
728
728
 
729
729
  # Do the actual construction.
730
- n = NMatrix.new(new_shape, opts)
730
+ n = NMatrix.new(new_shape, **opts)
731
731
 
732
732
  # Figure out where to start concatenation. We don't know where it will end,
733
733
  # because each matrix may have own size along concat dimension.
@@ -29,11 +29,10 @@ class NMatrix
29
29
  module VERSION #:nodoc:
30
30
  MAJOR = 2
31
31
  MINOR = 0
32
- TINY = 0
32
+ TINY = 1
33
33
  #PRE = "a"
34
34
 
35
35
  STRING = [MAJOR, MINOR, TINY].compact.join(".")
36
36
  #STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
37
37
  end
38
38
  end
39
-
data/spec/io_spec.rb CHANGED
@@ -51,6 +51,7 @@ describe NMatrix::IO do
51
51
  end
52
52
 
53
53
  it "reads MATLAB .mat file containing a single square sparse matrix" do
54
+ skip "Mat5Reader was removed from pnmatrix" unless defined?(NMatrix::IO::Matlab::Mat5Reader)
54
55
  pending("not yet implemented for NMatrix-JRuby") if jruby?
55
56
  n = NMatrix::IO::Matlab.load_mat("spec/4x4_sparse.mat")
56
57
  expect(n[0,0]).to eq(2)
@@ -62,19 +63,20 @@ describe NMatrix::IO do
62
63
  end
63
64
 
64
65
  it "reads MATLAB .mat file containing a single dense integer matrix" do
66
+ skip "Mat5Reader was removed from pnmatrix" unless defined?(NMatrix::IO::Matlab::Mat5Reader)
65
67
  n = NMatrix::IO::Matlab.load_mat("spec/4x5_dense.mat")
66
68
  m = NMatrix.new([4,5], [16,17,18,19,20,15,14,13,12,11,6,7,8,9,10,5,4,3,2,1])
67
69
  expect(n).to eq(m)
68
70
  end
69
71
 
70
72
  it "reads MATLAB .mat file containing a single dense double matrix" do
73
+ skip "Mat5Reader was removed from pnmatrix" unless defined?(NMatrix::IO::Matlab::Mat5Reader)
71
74
  n = NMatrix::IO::Matlab.load_mat("spec/2x2_dense_double.mat")
72
75
  m = NMatrix.new(2, [1.1, 2.0, 3.0, 4.0], dtype: :float64)
73
76
  expect(n).to eq(m)
74
77
  end
75
78
 
76
79
  it "loads and saves MatrixMarket .mtx file containing a single large sparse double matrix" do
77
- pending "spec disabled because it's so slow"
78
80
  n = NMatrix::IO::Market.load("spec/utm5940.mtx")
79
81
  NMatrix::IO::Market.save(n, "spec/utm5940.saved.mtx")
80
82
  expect(`wc -l spec/utm5940.mtx`.split[0]).to eq(`wc -l spec/utm5940.saved.mtx`.split[0])
@@ -273,8 +273,9 @@ describe "NMatrix::LAPACK functions with internal implementations" do
273
273
  end
274
274
 
275
275
  expect(s).to be_within(err).of(s_true)
276
- expect(u).to be_within(err).of(u_true)
277
- expect(vt).to be_within(err).of(vt_true)
276
+ # Singular vectors are only defined up to sign (or complex phase).
277
+ expect(u.abs).to be_within(err).of(u_true.abs)
278
+ expect(vt.abs).to be_within(err).of(vt_true.abs)
278
279
 
279
280
  expect(s.dtype).to eq(a.abs_dtype)
280
281
  expect(u.dtype).to eq(dtype)
@@ -306,8 +307,9 @@ describe "NMatrix::LAPACK functions with internal implementations" do
306
307
  end
307
308
 
308
309
  expect(s).to be_within(err).of(s_true)
309
- expect(u).to be_within(err).of(u_true)
310
- expect(vt).to be_within(err).of(vt_true)
310
+ # Singular vectors are only defined up to sign (or complex phase).
311
+ expect(u.abs).to be_within(err).of(u_true.abs)
312
+ expect(vt.abs).to be_within(err).of(vt_true.abs)
311
313
  end
312
314
 
313
315
 
@@ -337,8 +339,9 @@ describe "NMatrix::LAPACK functions with internal implementations" do
337
339
  end
338
340
 
339
341
  expect(eigenvalues).to be_within(err).of(eigenvalues_true)
340
- expect(vr).to be_within(err).of(vr_true)
341
- expect(vl).to be_within(err).of(vl_true)
342
+ # Eigenvectors are only defined up to sign (or complex phase).
343
+ expect(vr.abs).to be_within(err).of(vr_true.abs)
344
+ expect(vl.abs).to be_within(err).of(vl_true.abs)
342
345
 
343
346
  expect(eigenvalues.dtype).to eq(NMatrix.upcast(dtype, :complex64))
344
347
  expect(vr.dtype).to eq(NMatrix.upcast(dtype, :complex64))
@@ -384,7 +387,8 @@ describe "NMatrix::LAPACK functions with internal implementations" do
384
387
  end
385
388
 
386
389
  expect(eigenvalues).to be_within(err).of(eigenvalues_true)
387
- expect(vr).to be_within(err).of(vr_true)
390
+ # Eigenvectors are only defined up to sign (or complex phase).
391
+ expect(vr.abs).to be_within(err).of(vr_true.abs)
388
392
  expect(vl).to be_within(err).of(vl_true)
389
393
 
390
394
  expect(eigenvalues.dtype).to eq(dtype)
@@ -441,7 +445,8 @@ describe "NMatrix::LAPACK functions with internal implementations" do
441
445
  end
442
446
 
443
447
  expect(eigenvalues).to be_within(err).of(eigenvalues_true)
444
- expect(vr).to be_within(err).of(vr_true)
448
+ # Eigenvectors are only defined up to sign (or complex phase).
449
+ expect(vr.abs).to be_within(err).of(vr_true.abs)
445
450
  end
446
451
  end
447
452
  end
data/spec/math_spec.rb CHANGED
@@ -82,8 +82,10 @@ describe "math" do
82
82
  end
83
83
 
84
84
  it "should correctly apply elementwise natural log" do
85
- expect(@m.log).to eq N.new(@size, [0, Math.log(2), Math.log(3), Math.log(4)],
86
- dtype: :float64, stype: stype)
85
+ expect(@m.log).to be_within(1e-14).of(
86
+ N.new(@size, [0, Math.log(2), Math.log(3), Math.log(4)],
87
+ dtype: :float64, stype: stype)
88
+ )
87
89
  end
88
90
 
89
91
  it "should correctly apply elementwise log with arbitrary base" do
@@ -264,6 +266,7 @@ describe "math" do
264
266
  expect(r).to eq(b)
265
267
  rescue NotImplementedError
266
268
  pending "potrf! not implemented without plugins"
269
+ raise
267
270
  end
268
271
  end
269
272
 
@@ -280,6 +283,7 @@ describe "math" do
280
283
  expect(r).to eq(b)
281
284
  rescue NotImplementedError
282
285
  pending "potrf! not implemented without plugins"
286
+ raise
283
287
  end
284
288
  end
285
289
 
@@ -295,6 +299,7 @@ describe "math" do
295
299
  expect(l).to eq(l_true)
296
300
  rescue NotImplementedError
297
301
  pending "potrf! not implemented without plugins"
302
+ raise
298
303
  end
299
304
  end
300
305
  end
@@ -325,11 +330,13 @@ describe "math" do
325
330
  begin
326
331
  q,r = a.factorize_qr
327
332
 
328
- expect(q).to be_within(err).of(q_solution)
329
- expect(r).to be_within(err).of(r_solution)
333
+ # QR factors are only defined up to matched column/row signs.
334
+ expect(q.abs).to be_within(err).of(q_solution.abs)
335
+ expect(r.abs).to be_within(err).of(r_solution.abs)
330
336
 
331
337
  rescue NotImplementedError
332
338
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
339
+ raise
333
340
  end
334
341
  end
335
342
 
@@ -359,11 +366,13 @@ describe "math" do
359
366
  begin
360
367
  q,r = a.factorize_qr
361
368
 
362
- expect(q).to be_within(err).of(q_solution)
363
- expect(r).to be_within(err).of(r_solution)
369
+ # QR factors are only defined up to matched column/row signs.
370
+ expect(q.abs).to be_within(err).of(q_solution.abs)
371
+ expect(r.abs).to be_within(err).of(r_solution.abs)
364
372
 
365
373
  rescue NotImplementedError
366
374
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
375
+ raise
367
376
  end
368
377
  end
369
378
 
@@ -387,11 +396,13 @@ describe "math" do
387
396
  begin
388
397
  q,r = a.factorize_qr
389
398
 
390
- expect(q).to be_within(err).of(q_solution)
391
- expect(r).to be_within(err).of(r_solution)
399
+ # QR factors are only defined up to matched column/row signs.
400
+ expect(q.abs).to be_within(err).of(q_solution.abs)
401
+ expect(r.abs).to be_within(err).of(r_solution.abs)
392
402
 
393
403
  rescue NotImplementedError
394
404
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
405
+ raise
395
406
  end
396
407
  end
397
408
 
@@ -416,6 +427,7 @@ describe "math" do
416
427
 
417
428
  rescue NotImplementedError
418
429
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
430
+ raise
419
431
  end
420
432
  end
421
433
 
@@ -444,6 +456,7 @@ describe "math" do
444
456
 
445
457
  rescue NotImplementedError
446
458
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
459
+ raise
447
460
  end
448
461
  end
449
462
  end
@@ -461,7 +474,7 @@ describe "math" do
461
474
  end
462
475
 
463
476
  it "should correctly invert a matrix in place (bang)" do
464
- pending("not yet implemented for :object dtype") if dtype == :object
477
+ pending("not yet implemented for :object dtype") if dtype == :object && defined?(NMatrix::LAPACKE)
465
478
  a = NMatrix.new(:dense, 5, [1, 8,-9, 7, 5,
466
479
  0, 1, 0, 4, 4,
467
480
  0, 0, 1, 2, 5,
@@ -529,6 +542,7 @@ describe "math" do
529
542
  expect(a.dot(a.pinv)).to be_within(err).of(b)
530
543
  rescue NotImplementedError
531
544
  pending "Suppressing a NotImplementedError when the atlas plugin is not available"
545
+ raise
532
546
  end
533
547
 
534
548
  else
@@ -539,6 +553,7 @@ describe "math" do
539
553
  expect(a.dot(a.pinv)).to be_within(err).of(b)
540
554
  rescue NotImplementedError
541
555
  pending "Suppressing a NotImplementedError when the atlas plugin is not available"
556
+ raise
542
557
  end
543
558
  end
544
559
  end
@@ -556,6 +571,7 @@ describe "math" do
556
571
  expect(b.dot(a.dot(b))).to be_within(err).of(b)
557
572
  rescue NotImplementedError
558
573
  pending "Suppressing a NotImplementedError when the atlas plugin is not available"
574
+ raise
559
575
  end
560
576
 
561
577
  else
@@ -567,6 +583,7 @@ describe "math" do
567
583
  expect(b.dot(a.dot(b))).to be_within(err).of(b)
568
584
  rescue NotImplementedError
569
585
  pending "Suppressing a NotImplementedError when the atlas plugin is not available"
586
+ raise
570
587
  end
571
588
  end
572
589
  end
@@ -1134,7 +1151,6 @@ describe "math" do
1134
1151
  expect(@c.det).to be_within(@err).of(-18)
1135
1152
  end
1136
1153
  it "computes the exact determinant of 2x2 matrix" do
1137
- pending("not yet implemented for :object dtype") if dtype == :object
1138
1154
  if dtype == :byte
1139
1155
  expect{@a.det_exact}.to raise_error(DataTypeError)
1140
1156
  else
@@ -1166,7 +1182,6 @@ describe "math" do
1166
1182
  end
1167
1183
 
1168
1184
  it "scales the matrix by a given factor and return the result" do
1169
- pending("not yet implemented for :object dtype") if dtype == :object
1170
1185
  if integer_dtype? dtype
1171
1186
  expect{@m.scale 2.0}.to raise_error(DataTypeError)
1172
1187
  else
@@ -1178,7 +1193,6 @@ describe "math" do
1178
1193
  end
1179
1194
 
1180
1195
  it "scales the matrix in place by a given factor" do
1181
- pending("not yet implemented for :object dtype") if dtype == :object
1182
1196
  if dtype == :int8
1183
1197
  expect{@m.scale! 2}.to raise_error(DataTypeError)
1184
1198
  else
@@ -1196,7 +1210,6 @@ describe "math" do
1196
1210
  context "matrix_norm" do
1197
1211
  ALL_DTYPES.each do |dtype|
1198
1212
  context dtype do
1199
- pending("not yet implemented for :object dtype") if dtype == :object
1200
1213
  before do
1201
1214
  @n = NMatrix.new([3,3], [-4,-3,-2,
1202
1215
  -1, 0, 1,
@@ -1215,6 +1228,7 @@ describe "math" do
1215
1228
 
1216
1229
  rescue NotImplementedError
1217
1230
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1231
+ raise
1218
1232
  end
1219
1233
  end
1220
1234
  end
@@ -1240,6 +1254,7 @@ describe "math" do
1240
1254
  expect(@n.matrix_norm(-2)).to be_within(@matrix_norm_TOLERANCE).of(0.0)
1241
1255
  rescue NotImplementedError
1242
1256
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1257
+ raise
1243
1258
  end
1244
1259
  expect(@n.matrix_norm(-1)).to eq(6)
1245
1260
  end
@@ -1293,6 +1308,7 @@ describe "math" do
1293
1308
 
1294
1309
  rescue NotImplementedError
1295
1310
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1311
+ raise
1296
1312
  end
1297
1313
  end
1298
1314
 
@@ -1308,6 +1324,7 @@ describe "math" do
1308
1324
 
1309
1325
  rescue NotImplementedError
1310
1326
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1327
+ raise
1311
1328
  end
1312
1329
  end
1313
1330
 
@@ -1323,6 +1340,7 @@ describe "math" do
1323
1340
 
1324
1341
  rescue NotImplementedError
1325
1342
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1343
+ raise
1326
1344
  end
1327
1345
  end
1328
1346
 
@@ -1338,6 +1356,7 @@ describe "math" do
1338
1356
 
1339
1357
  rescue NotImplementedError
1340
1358
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1359
+ raise
1341
1360
  end
1342
1361
  end
1343
1362
 
@@ -1353,6 +1372,7 @@ describe "math" do
1353
1372
 
1354
1373
  rescue NotImplementedError
1355
1374
  pending "Suppressing a NotImplementedError when the lapacke plugin is not available"
1375
+ raise
1356
1376
  end
1357
1377
  end
1358
1378
 
@@ -1,6 +1,94 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe NMatrix do
4
+ def force_gc
5
+ 20.times do
6
+ Array.new(1_000) { |i| "gc-pressure-#{i}" }
7
+ GC.start
8
+ end
9
+ end
10
+
11
+ def build_dense_object_reference
12
+ values = Array.new(64 * 64) { |i| "dense-object-#{i}" }
13
+ matrix = NMatrix.new([64, 64], values, stype: :dense, dtype: :object)
14
+
15
+ matrix[50...64, 45...64]
16
+ end
17
+
18
+ def build_yale_object_reference
19
+ rng = Random.new(5678)
20
+ matrix = NMatrix.new([35, 35], nil, stype: :yale, dtype: :object, capacity: 1)
21
+
22
+ 700.times do |i|
23
+ matrix[rng.rand(35), rng.rand(35)] = "yale-object-#{i}"
24
+ end
25
+
26
+ matrix[3...30, 4...32]
27
+ end
28
+
29
+ def build_nested_yale_object_reference
30
+ rng = Random.new(6789)
31
+ matrix = NMatrix.new([45, 45], nil, stype: :yale, dtype: :object, capacity: 1)
32
+
33
+ 1_000.times do |i|
34
+ matrix[rng.rand(45), rng.rand(45)] = "nested-yale-object-#{i}"
35
+ end
36
+
37
+ matrix[5...40, 6...39][7...28, 8...30]
38
+ end
39
+
40
+ def build_list_object_reference
41
+ matrix = NMatrix.new([30, 30], nil, stype: :list, dtype: :object)
42
+
43
+ 30.times do |i|
44
+ matrix[i, (i * 7) % 30] = "list-object-#{i}"
45
+ end
46
+
47
+ matrix[4...28, 2...29]
48
+ end
49
+
50
+ def mutate_dense_object_matrix_without_external_references
51
+ rng = Random.new(555)
52
+ matrix = NMatrix.new([80, 80], nil, stype: :dense, dtype: :object)
53
+
54
+ 5_000.times do |i|
55
+ matrix[rng.rand(80), rng.rand(80)] = "mut-dense-#{i}"
56
+ GC.start if (i % 25).zero?
57
+ end
58
+
59
+ matrix
60
+ end
61
+
62
+ def mutate_yale_object_matrix_without_external_references
63
+ rng = Random.new(333)
64
+ matrix = NMatrix.new([80, 80], nil, stype: :yale, dtype: :object, capacity: 1)
65
+
66
+ 5_000.times do |i|
67
+ matrix[rng.rand(80), rng.rand(80)] = "mut-yale-#{i}"
68
+ GC.start if (i % 25).zero?
69
+ end
70
+
71
+ matrix
72
+ end
73
+
74
+ def mutate_list_object_matrix_without_external_references
75
+ rng = Random.new(888)
76
+ matrix = NMatrix.new([30, 30], nil, stype: :list, dtype: :object)
77
+
78
+ 500.times do |i|
79
+ matrix[rng.rand(30), rng.rand(30)] = "mut-list-#{i}"
80
+ GC.start if (i % 10).zero?
81
+ end
82
+
83
+ matrix
84
+ end
85
+
86
+ def touch_reference(reference)
87
+ reference.each_stored_with_indices do |value, _i, _j|
88
+ value.to_s.hash
89
+ end
90
+ end
91
+
4
92
  specify do
5
93
  200.times do |i|
6
94
  size = rand(500..1000)
@@ -15,4 +103,79 @@ describe NMatrix do
15
103
  (m1*m2).det
16
104
  end
17
105
  end
106
+
107
+ specify "does not segfault when iterating a GC-stressed object Yale reference" do
108
+ reference = build_yale_object_reference
109
+
110
+ force_gc
111
+
112
+ touch_reference(reference)
113
+ end
114
+
115
+ specify "does not segfault when iterating a GC-stressed nested object Yale reference" do
116
+ reference = build_nested_yale_object_reference
117
+
118
+ force_gc
119
+
120
+ touch_reference(reference)
121
+ end
122
+
123
+ specify "does not segfault when iterating a GC-stressed object dense reference" do
124
+ reference = build_dense_object_reference
125
+
126
+ force_gc
127
+
128
+ reference.each_with_indices do |value, _i, _j|
129
+ value.to_s.hash
130
+ end
131
+ end
132
+
133
+ specify "does not segfault when iterating a GC-stressed object list reference" do
134
+ reference = build_list_object_reference
135
+
136
+ force_gc
137
+
138
+ touch_reference(reference)
139
+ end
140
+
141
+ specify "does not segfault after GC-stressed object dense mutation" do
142
+ matrix = mutate_dense_object_matrix_without_external_references
143
+
144
+ force_gc
145
+
146
+ matrix.each do |value|
147
+ value.to_s.hash
148
+ end
149
+ end
150
+
151
+ specify "does not segfault after GC-stressed object Yale mutation" do
152
+ matrix = mutate_yale_object_matrix_without_external_references
153
+
154
+ force_gc
155
+
156
+ touch_reference(matrix)
157
+ end
158
+
159
+ specify "does not segfault after GC-stressed object list mutation" do
160
+ matrix = mutate_list_object_matrix_without_external_references
161
+
162
+ force_gc
163
+
164
+ touch_reference(matrix)
165
+ end
166
+
167
+ specify "does not expose partially built Yale object storage during a cast" do
168
+ previous_gc_stress = GC.stress
169
+ GC.stress = true
170
+
171
+ current_stock = NMatrix.new([30, 1], 0, stype: :yale, dtype: :float64)
172
+ current_stock[3] = -4
173
+ current_stock[7] = 10
174
+ current_stock[11] = 50
175
+
176
+ quantity_made = (current_stock < 0).cast(dtype: :int32) * current_stock * -1
177
+ expect(quantity_made[3]).to eq(4)
178
+ ensure
179
+ GC.stress = previous_gc_stress
180
+ end
18
181
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pnmatrix
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Woods