Tpetra parallel linear algebra Version of the Day
Loading...
Searching...
No Matches
Tpetra_Details_crsUtils.hpp
Go to the documentation of this file.
1// @HEADER
2// *****************************************************************************
3// Tpetra: Templated Linear Algebra Services Package
4//
5// Copyright 2008 NTESS and the Tpetra contributors.
6// SPDX-License-Identifier: BSD-3-Clause
7// *****************************************************************************
8// @HEADER
9
10#ifndef TPETRA_DETAILS_CRSUTILS_HPP
11#define TPETRA_DETAILS_CRSUTILS_HPP
12#include <numeric>
13#include <type_traits>
14
15#include "TpetraCore_config.h"
16#include "Kokkos_Core.hpp"
18#include "Tpetra_Details_CrsPadding.hpp"
19#include "Tpetra_Details_WrappedDualView.hpp"
20#include <iostream>
21#include <memory>
22#include <unordered_map>
23#include <algorithm>
24
30
31namespace Tpetra {
32namespace Details {
33
34namespace impl {
35
36template <class ViewType>
37ViewType
38make_uninitialized_view(
39 const std::string& name,
40 const size_t size,
41 const bool verbose,
42 const std::string* const prefix) {
43 if (verbose) {
44 std::ostringstream os;
45 os << *prefix << "Allocate Kokkos::View " << name
46 << ": " << size << std::endl;
47 std::cerr << os.str();
48 }
49 using Kokkos::view_alloc;
50 using Kokkos::WithoutInitializing;
51 return ViewType(view_alloc(name, WithoutInitializing), size);
52}
53
54template <class ViewType>
55ViewType
56make_initialized_view(
57 const std::string& name,
58 const size_t size,
59 const bool verbose,
60 const std::string* const prefix) {
61 if (verbose) {
62 std::ostringstream os;
63 os << *prefix << "Allocate & initialize Kokkos::View "
64 << name << ": " << size << std::endl;
65 std::cerr << os.str();
66 }
67 return ViewType(name, size);
68}
69
70template <class OutViewType, class InViewType>
71void assign_to_view(OutViewType& out,
72 const InViewType& in,
73 const char viewName[],
74 const bool verbose,
75 const std::string* const prefix) {
76 if (verbose) {
77 std::ostringstream os;
78 os << *prefix << "Assign to Kokkos::View " << viewName
79 << ": Old size: " << out.extent(0)
80 << ", New size: " << in.extent(0) << std::endl;
81 std::cerr << os.str();
82 }
83 out = in;
84}
85
86template <class MemorySpace, class ViewType>
87auto create_mirror_view(
88 const MemorySpace& memSpace,
89 const ViewType& view,
90 const bool verbose,
91 const std::string* const prefix) -> decltype(Kokkos::create_mirror_view(memSpace, view)) {
92 if (verbose) {
93 std::ostringstream os;
94 os << *prefix << "Create mirror view: "
95 << "view.extent(0): " << view.extent(0) << std::endl;
96 std::cerr << os.str();
97 }
98 return Kokkos::create_mirror_view(memSpace, view);
99}
100
101enum class PadCrsAction {
102 INDICES_ONLY,
103 INDICES_AND_VALUES
104};
105
114template <class RowPtr, class Indices, class Values, class Padding>
116 const PadCrsAction action,
117 const RowPtr& row_ptr_beg,
118 const RowPtr& row_ptr_end,
121 const Padding& padding,
122 const int my_rank,
123 const bool verbose) {
124 using execution_space = typename Indices::t_dev::execution_space;
125 using Kokkos::view_alloc;
126 using Kokkos::WithoutInitializing;
127 using std::endl;
128 std::unique_ptr<std::string> prefix;
129
130 const size_t maxNumToPrint = verbose ? Behavior::verbosePrintCountThreshold() : size_t(0);
131 if (verbose) {
132 std::ostringstream os;
133 os << "Proc " << my_rank << ": Tpetra::...::pad_crs_arrays: ";
134 prefix = std::unique_ptr<std::string>(new std::string(os.str()));
135 os << "Start" << endl;
136 std::cerr << os.str();
137 }
138 Kokkos::HostSpace hostSpace;
139
140 if (verbose) {
141 std::ostringstream os;
142 os << *prefix << "On input: ";
143 auto row_ptr_beg_h =
144 Kokkos::create_mirror_view(hostSpace, row_ptr_beg);
145 // DEEP_COPY REVIEW - NOT TESTED
146 Kokkos::deep_copy(row_ptr_beg_h, row_ptr_beg);
147 verbosePrintArray(os, row_ptr_beg_h, "row_ptr_beg before scan",
149 os << ", ";
150 auto row_ptr_end_h =
151 Kokkos::create_mirror_view(hostSpace, row_ptr_end);
152 // DEEP_COPY REVIEW - NOT TESTED
153 Kokkos::deep_copy(row_ptr_end_h, row_ptr_end);
154 verbosePrintArray(os, row_ptr_end_h, "row_ptr_end before scan",
156 os << ", indices.extent(0): " << indices_wdv.extent(0)
157 << ", values.extent(0): " << values_wdv.extent(0)
158 << ", padding: ";
159 padding.print(os);
160 os << endl;
161 std::cerr << os.str();
162 }
163
164 if (row_ptr_beg.size() == 0) {
165 if (verbose) {
166 std::ostringstream os;
167 os << *prefix << "Done; local matrix has no rows" << endl;
168 std::cerr << os.str();
169 }
170 return; // nothing to do
171 }
172
173 const size_t lclNumRows(row_ptr_beg.size() - 1);
176 verbose, prefix.get());
177 if (verbose) {
178 std::ostringstream os;
179 os << *prefix << "Fill newAllocPerRow & compute increase" << endl;
180 std::cerr << os.str();
181 }
182 size_t increase = 0;
183 {
184 // Must do on host because padding uses std::map
185 execution_space exec_space_instance = execution_space();
186 auto row_ptr_end_h = create_mirror_view(
187 hostSpace, row_ptr_end, verbose, prefix.get());
188 // DEEP_COPY REVIEW - DEVICE-TO-HOSTMIRROR
189 Kokkos::deep_copy(exec_space_instance, row_ptr_end_h, row_ptr_end);
190 auto row_ptr_beg_h = create_mirror_view(
191 hostSpace, row_ptr_beg, verbose, prefix.get());
192 // DEEP_COPY REVIEW - DEVICE-TO-HOSTMIRROR
193 Kokkos::deep_copy(exec_space_instance, row_ptr_beg_h, row_ptr_beg);
194
195 // lbv 03/15/23: The execution space deep_copy does an asynchronous
196 // copy so we really want to fence that space before touching the
197 // host data as it is not guarenteed to have arrived by the time we
198 // start the parallel_reduce below which might use a different
199 // execution space, see:
200 // https://kokkos.github.io/kokkos-core-wiki/API/core/view/deep_copy.html#semantics
201 exec_space_instance.fence();
202
203 auto newAllocPerRow_h = create_mirror_view(
204 hostSpace, newAllocPerRow, verbose, prefix.get());
205 using host_range_type = Kokkos::RangePolicy<
206 Kokkos::DefaultHostExecutionSpace, size_t>;
207 Kokkos::parallel_reduce(
208 "Tpetra::CrsGraph: Compute new allocation size per row",
210 [&](const size_t lclRowInd, size_t& lclIncrease) {
211 const size_t start = row_ptr_beg_h[lclRowInd];
212 const size_t end = row_ptr_beg_h[lclRowInd + 1];
213 TEUCHOS_ASSERT(end >= start);
214 const size_t oldAllocSize = end - start;
215 const size_t oldNumEnt = row_ptr_end_h[lclRowInd] - start;
217
218 // This is not a pack routine. Do not shrink! Shrinking now
219 // to fit the number of entries would ignore users' hint for
220 // the max number of entries in each row. Also, CrsPadding
221 // only counts entries and ignores any available free space.
222
223 auto result = padding.get_result(lclRowInd);
224 const size_t newNumEnt = oldNumEnt + result.numInSrcNotInTgt;
225 if (newNumEnt > oldAllocSize) {
228 } else {
230 }
231 },
232 increase);
233
234 if (verbose) {
235 std::ostringstream os;
236 os << *prefix << "increase: " << increase << ", ";
237 verbosePrintArray(os, newAllocPerRow_h, "newAllocPerRow",
239 os << endl;
240 std::cerr << os.str();
241 }
242
243 if (increase == 0) {
244 return;
245 }
246 // DEEP_COPY REVIEW - HOSTMIRROR-TO-DEVICE
247 Kokkos::deep_copy(execution_space(), newAllocPerRow, newAllocPerRow_h);
248 }
249
250 using inds_value_type =
251 typename Indices::t_dev::non_const_value_type;
252 using vals_value_type = typename Values::t_dev::non_const_value_type;
253
254 {
255 auto indices_old = indices_wdv.getDeviceView(Access::ReadOnly);
256 const size_t newIndsSize = size_t(indices_old.size()) + increase;
258 "Tpetra::CrsGraph column indices", newIndsSize, verbose,
259 prefix.get());
260
261 typename Values::t_dev values_new;
262 auto values_old = values_wdv.getDeviceView(Access::ReadOnly);
263 if (action == PadCrsAction::INDICES_AND_VALUES) {
264 const size_t newValsSize = newIndsSize;
265 // NOTE (mfh 10 Feb 2020) If we don't initialize values_new here,
266 // then the CrsMatrix tests fail.
268 "Tpetra::CrsMatrix values", newValsSize, verbose, prefix.get());
269 }
270
271 if (verbose) {
272 std::ostringstream os;
273 os << *prefix << "Repack" << endl;
274 std::cerr << os.str();
275 }
276
277 using range_type = Kokkos::RangePolicy<execution_space, size_t>;
278 Kokkos::parallel_scan(
279 "Tpetra::CrsGraph or CrsMatrix repack",
280 range_type(size_t(0), size_t(lclNumRows + 1)),
281 KOKKOS_LAMBDA(const size_t lclRow, size_t& newRowBeg,
282 const bool finalPass) {
283 // row_ptr_beg has lclNumRows + 1 entries.
284 // row_ptr_end has lclNumRows entries.
285 // newAllocPerRow has lclNumRows entries.
286 const size_t row_beg = row_ptr_beg[lclRow];
287 const size_t row_end =
289 const size_t numEnt = row_end - row_beg;
290 const size_t newRowAllocSize =
292 if (finalPass) {
293 if (lclRow < lclNumRows) {
294 const Kokkos::pair<size_t, size_t> oldRange(
296 const Kokkos::pair<size_t, size_t> newRange(
298 auto oldColInds = Kokkos::subview(indices_old, oldRange);
299 auto newColInds = Kokkos::subview(indices_new, newRange);
300 // memcpy works fine on device; the next step is to
301 // introduce two-level parallelism and use team copy.
302 memcpy(newColInds.data(), oldColInds.data(),
303 numEnt * sizeof(inds_value_type));
304 if (action == PadCrsAction::INDICES_AND_VALUES) {
305 auto oldVals =
306 Kokkos::subview(values_old, oldRange);
307 auto newVals = Kokkos::subview(values_new, newRange);
308 memcpy((void*)newVals.data(), oldVals.data(),
309 numEnt * sizeof(vals_value_type));
310 }
311 }
312 // It's the final pass, so we can modify these arrays.
314 if (lclRow < lclNumRows) {
316 }
317 }
319 });
320
321 if (verbose) {
322 std::ostringstream os;
323
324 os << *prefix;
325 auto row_ptr_beg_h =
326 Kokkos::create_mirror_view(hostSpace, row_ptr_beg);
327 // DEEP_COPY REVIEW - NOT TESTED
328 Kokkos::deep_copy(row_ptr_beg_h, row_ptr_beg);
329 verbosePrintArray(os, row_ptr_beg_h, "row_ptr_beg after scan",
331 os << endl;
332
333 os << *prefix;
334 auto row_ptr_end_h =
335 Kokkos::create_mirror_view(hostSpace, row_ptr_end);
336 // DEEP_COPY REVIEW - NOT TESTED
337 Kokkos::deep_copy(row_ptr_end_h, row_ptr_end);
338 verbosePrintArray(os, row_ptr_end_h, "row_ptr_end after scan",
340 os << endl;
341
342 std::cout << os.str();
343 }
344
347 }
348
349 if (verbose) {
350 auto indices_h = indices_wdv.getHostView(Access::ReadOnly);
351 auto values_h = values_wdv.getHostView(Access::ReadOnly);
352 std::ostringstream os;
353 os << "On output: ";
355 os << ", ";
357 os << ", padding: ";
358 padding.print(os);
359 os << endl;
360 }
361
362 if (verbose) {
363 std::ostringstream os;
364 os << *prefix << "Done" << endl;
365 std::cerr << os.str();
366 }
367}
368
370template <class Pointers, class InOutIndices, class InIndices, class IndexMap>
371size_t
373 typename Pointers::value_type const row,
374 Pointers const& row_ptrs,
376 size_t& num_assigned,
377 InIndices const& new_indices,
378 IndexMap&& map,
379 std::function<void(size_t const, size_t const, size_t const)> cb) {
380 if (new_indices.size() == 0) {
381 return 0;
382 }
383
384 if (cur_indices.size() == 0) {
385 // No room to insert new indices
386 return Teuchos::OrdinalTraits<size_t>::invalid();
387 }
388
389 using offset_type = typename std::decay<decltype(row_ptrs[0])>::type;
390 using ordinal_type = typename std::decay<decltype(cur_indices[0])>::type;
391
392 const offset_type start = row_ptrs[row];
393 offset_type end = start + static_cast<offset_type>(num_assigned);
394 const size_t num_avail = (row_ptrs[row + 1] < end) ? size_t(0) : row_ptrs[row + 1] - end;
395 const size_t num_new_indices = static_cast<size_t>(new_indices.size());
396 size_t num_inserted = 0;
397
399
400 // Threshold determined from test/Utils/insertCrsIndicesThreshold.cpp
401 const size_t useLookUpTableThreshold = 400;
402
404 // For rows with few nonzeros, can use a serial search to find duplicates
405 // Or if inserting only one index, serial search is as fast as anything else
406 for (size_t k = 0; k < num_new_indices; ++k) {
407 const ordinal_type idx = std::forward<IndexMap>(map)(new_indices[k]);
408 offset_type row_offset = start;
409 for (; row_offset < end; ++row_offset) {
410 if (idx == cur_indices[row_offset]) {
411 break;
412 }
413 }
414
415 if (row_offset == end) {
416 if (num_inserted >= num_avail) { // not enough room
417 return Teuchos::OrdinalTraits<size_t>::invalid();
418 }
419 // This index is not yet in indices
420 cur_indices[end++] = idx;
421 num_inserted++;
422 }
423 if (cb) {
424 cb(k, start, row_offset - start);
425 }
426 }
427 } else {
428 // For rows with many nonzeros, use a lookup table to find duplicates
429 std::unordered_map<ordinal_type, offset_type> idxLookup(numIndicesLookup);
430
431 // Put existing indices into the lookup table
432 for (size_t k = 0; k < num_assigned; k++) {
433 idxLookup[cur_indices[start + k]] = start + k;
434 }
435
436 // Check for new indices in table; insert if not there yet
437 for (size_t k = 0; k < num_new_indices; k++) {
438 const ordinal_type idx = std::forward<IndexMap>(map)(new_indices[k]);
439 offset_type row_offset;
440
441 auto it = idxLookup.find(idx);
442 if (it == idxLookup.end()) {
443 if (num_inserted >= num_avail) { // not enough room
444 return Teuchos::OrdinalTraits<size_t>::invalid();
445 }
446 // index not found; insert it
447 row_offset = end;
448 cur_indices[end++] = idx;
449 idxLookup[idx] = row_offset;
450 num_inserted++;
451 } else {
452 // index found; note its position
453 row_offset = it->second;
454 }
455 if (cb) {
456 cb(k, start, row_offset - start);
457 }
458 }
459 }
461 return num_inserted;
462}
463
465template <class Pointers, class Indices1, class Indices2, class IndexMap, class Callback>
466size_t
468 typename Pointers::value_type const row,
469 Pointers const& row_ptrs,
470 const size_t curNumEntries,
471 Indices1 const& cur_indices,
472 Indices2 const& new_indices,
473 IndexMap&& map,
474 Callback&& cb) {
475 if (new_indices.size() == 0)
476 return 0;
477
478 using ordinal =
479 typename std::remove_const<typename Indices1::value_type>::type;
480 auto invalid_ordinal = Teuchos::OrdinalTraits<ordinal>::invalid();
481
482 const size_t start = static_cast<size_t>(row_ptrs[row]);
483 const size_t end = start + curNumEntries;
484 size_t num_found = 0;
485 for (size_t k = 0; k < new_indices.size(); k++) {
486 auto idx = std::forward<IndexMap>(map)(new_indices[k]);
487 if (idx == invalid_ordinal)
488 continue;
489 for (size_t row_offset = start; row_offset < end; row_offset++) {
490 size_t off = row_offset - start;
492 if (idx == lidx) {
493 std::forward<Callback>(cb)(k, start, off);
494 num_found++;
495 }
496 }
497 }
498 return num_found;
499}
500
502template <class Pointers, class Indices1, class Indices2, class IndexMap, class Callback>
504 typename Pointers::value_type const row,
505 Pointers const& row_ptrs,
506 const size_t curNumEntries,
507 Indices1 const& cur_indices,
508 Indices2 const& new_indices,
509 IndexMap&& map,
510 Callback&& cb) {
511 if (new_indices.size() == 0)
512 return 0;
513
514 using ordinal = typename std::remove_const<typename Indices1::value_type>::type;
515 auto invalid_ordinal = Teuchos::OrdinalTraits<ordinal>::invalid();
516
517 const size_t start = static_cast<size_t>(row_ptrs[row]);
518 const size_t end = start + curNumEntries;
519 size_t num_found = 0;
520 for (size_t k = 0; k < new_indices.size(); k++) {
521 auto idx = std::forward<IndexMap>(map)(new_indices[k]);
522 if (idx == invalid_ordinal)
523 continue;
524
525 // FIXME use kokkos findRelOffset
526 auto first = &cur_indices[start];
527 auto first0 = first;
528 auto last = &cur_indices[end];
529 first = std::lower_bound(first, last, idx);
530 size_t off = first - first0;
531 if (first != last && !(idx < *first)) {
532 std::forward<Callback>(cb)(k, start, off);
533 num_found++;
534 }
535 }
536 return num_found;
537}
538
539} // namespace impl
540
558template <class RowPtr, class Indices, class Padding>
560 const RowPtr& rowPtrBeg,
561 const RowPtr& rowPtrEnd,
563 const Padding& padding,
564 const int my_rank,
565 const bool verbose) {
566 using impl::pad_crs_arrays;
567 // send empty values array
570 impl::PadCrsAction::INDICES_ONLY, rowPtrBeg, rowPtrEnd,
572}
573
574template <class RowPtr, class Indices, class Values, class Padding>
575void padCrsArrays(
576 const RowPtr& rowPtrBeg,
577 const RowPtr& rowPtrEnd,
580 const Padding& padding,
581 const int my_rank,
582 const bool verbose) {
583 using impl::pad_crs_arrays;
585 impl::PadCrsAction::INDICES_AND_VALUES, rowPtrBeg, rowPtrEnd,
587}
588
634template <class Pointers, class InOutIndices, class InIndices>
635size_t
637 typename Pointers::value_type const row,
638 Pointers const& rowPtrs,
640 size_t& numAssigned,
641 InIndices const& newIndices,
642 std::function<void(const size_t, const size_t, const size_t)> cb =
643 std::function<void(const size_t, const size_t, const size_t)>()) {
644 static_assert(std::is_same<typename std::remove_const<typename InOutIndices::value_type>::type,
645 typename std::remove_const<typename InIndices::value_type>::type>::value,
646 "Expected views to have same value type");
647
648 // Provide a unit map for the more general insert_indices
649 using ordinal = typename InOutIndices::value_type;
650 auto numInserted = impl::insert_crs_indices(
651 row, rowPtrs, curIndices,
652 numAssigned, newIndices, [](ordinal const idx) { return idx; }, cb);
653 return numInserted;
654}
655
656template <class Pointers, class InOutIndices, class InIndices>
657size_t
659 typename Pointers::value_type const row,
660 Pointers const& rowPtrs,
662 size_t& numAssigned,
663 InIndices const& newIndices,
664 std::function<typename InOutIndices::value_type(const typename InIndices::value_type)> map,
665 std::function<void(const size_t, const size_t, const size_t)> cb =
666 std::function<void(const size_t, const size_t, const size_t)>()) {
667 auto numInserted = impl::insert_crs_indices(row, rowPtrs, curIndices,
669 return numInserted;
670}
671
701template <class Pointers, class Indices1, class Indices2, class Callback>
702size_t
704 typename Pointers::value_type const row,
705 Pointers const& rowPtrs,
706 const size_t curNumEntries,
707 Indices1 const& curIndices,
708 Indices2 const& newIndices,
709 Callback&& cb) {
710 static_assert(std::is_same<typename std::remove_const<typename Indices1::value_type>::type,
711 typename std::remove_const<typename Indices2::value_type>::type>::value,
712 "Expected views to have same value type");
713 // Provide a unit map for the more general find_crs_indices
714 using ordinal = typename Indices2::value_type;
715 auto numFound = impl::find_crs_indices(
717 [=](ordinal ind) { return ind; }, cb);
718 return numFound;
719}
720
721template <class Pointers, class Indices1, class Indices2, class IndexMap, class Callback>
722size_t
724 typename Pointers::value_type const row,
725 Pointers const& rowPtrs,
726 const size_t curNumEntries,
727 Indices1 const& curIndices,
728 Indices2 const& newIndices,
729 IndexMap&& map,
730 Callback&& cb) {
731 return impl::find_crs_indices(row, rowPtrs, curNumEntries, curIndices, newIndices, map, cb);
732}
733
734template <class Pointers, class Indices1, class Indices2, class IndexMap, class Callback>
735size_t findCrsIndicesSorted(
736 typename Pointers::value_type const row,
737 Pointers const& rowPtrs,
738 const size_t curNumEntries,
739 Indices1 const& curIndices,
740 Indices2 const& newIndices,
741 IndexMap&& map,
742 Callback&& cb) {
743 return impl::find_crs_indices_sorted(row, rowPtrs, curNumEntries, curIndices, newIndices, map, cb);
744}
745
746} // namespace Details
747} // namespace Tpetra
748
749#endif // TPETRA_DETAILS_CRSUTILS_HPP
Declaration of Tpetra::Details::Behavior, a class that describes Tpetra's behavior.
void pad_crs_arrays(const PadCrsAction action, const RowPtr &row_ptr_beg, const RowPtr &row_ptr_end, Indices &indices_wdv, Values &values_wdv, const Padding &padding, const int my_rank, const bool verbose)
Implementation of padCrsArrays.
size_t find_crs_indices(typename Pointers::value_type const row, Pointers const &row_ptrs, const size_t curNumEntries, Indices1 const &cur_indices, Indices2 const &new_indices, IndexMap &&map, Callback &&cb)
Implementation of findCrsIndices.
size_t find_crs_indices_sorted(typename Pointers::value_type const row, Pointers const &row_ptrs, const size_t curNumEntries, Indices1 const &cur_indices, Indices2 const &new_indices, IndexMap &&map, Callback &&cb)
Implementation of findCrsIndices.
size_t insert_crs_indices(typename Pointers::value_type const row, Pointers const &row_ptrs, InOutIndices &cur_indices, size_t &num_assigned, InIndices const &new_indices, IndexMap &&map, std::function< void(size_t const, size_t const, size_t const)> cb)
Implementation of insertCrsIndices.
Struct that holds views of the contents of a CrsMatrix.
static size_t verbosePrintCountThreshold()
Number of entries below which arrays, lists, etc. will be printed in debug mode.
Implementation details of Tpetra.
void padCrsArrays(const RowPtr &rowPtrBeg, const RowPtr &rowPtrEnd, Indices &indices_wdv, const Padding &padding, const int my_rank, const bool verbose)
Determine if the row pointers and indices arrays need to be resized to accommodate new entries....
void verbosePrintArray(std::ostream &out, const ArrayType &x, const char name[], const size_t maxNumToPrint)
Print min(x.size(), maxNumToPrint) entries of x.
size_t insertCrsIndices(typename Pointers::value_type const row, Pointers const &rowPtrs, InOutIndices &curIndices, size_t &numAssigned, InIndices const &newIndices, std::function< void(const size_t, const size_t, const size_t)> cb=std::function< void(const size_t, const size_t, const size_t)>())
Insert new indices in to current list of indices.
size_t findCrsIndices(typename Pointers::value_type const row, Pointers const &rowPtrs, const size_t curNumEntries, Indices1 const &curIndices, Indices2 const &newIndices, Callback &&cb)
Finds offsets in to current list of indices.
Namespace Tpetra contains the class and methods constituting the Tpetra library.