| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-FileCopyrightText: 2022-2023 Dennis Gläser <dennis.glaeser@iws.uni-stuttgart.de> | ||
| 2 | // SPDX-License-Identifier: MIT | ||
| 3 | /*! | ||
| 4 | * \file | ||
| 5 | * \ingroup Grid | ||
| 6 | * \brief Converter between grid formats. | ||
| 7 | */ | ||
| 8 | #ifndef GRIDFORMAT_GRID_CONVERTER_HPP_ | ||
| 9 | #define GRIDFORMAT_GRID_CONVERTER_HPP_ | ||
| 10 | |||
| 11 | #include <array> | ||
| 12 | #include <vector> | ||
| 13 | #include <ranges> | ||
| 14 | #include <concepts> | ||
| 15 | #include <utility> | ||
| 16 | #include <numeric> | ||
| 17 | #include <optional> | ||
| 18 | #include <cstdint> | ||
| 19 | |||
| 20 | #include <gridformat/common/field.hpp> | ||
| 21 | #include <gridformat/common/exceptions.hpp> | ||
| 22 | |||
| 23 | #include <gridformat/grid/cell_type.hpp> | ||
| 24 | #include <gridformat/grid/reader.hpp> | ||
| 25 | #include <gridformat/grid/writer.hpp> | ||
| 26 | |||
| 27 | namespace GridFormat { | ||
| 28 | |||
| 29 | #ifndef DOXYGEN | ||
| 30 | namespace ConverterDetail { | ||
| 31 | |||
| 32 | struct ConverterGrid { | ||
| 33 | const GridReader& reader; | ||
| 34 | std::vector<std::array<double, 3>> points; | ||
| 35 | std::vector<std::pair<CellType, std::vector<std::size_t>>> cells; | ||
| 36 | |||
| 37 | 35 | explicit ConverterGrid(const GridReader& r) : reader{r} {} | |
| 38 | |||
| 39 | 81 | void make_grid() { | |
| 40 | 81 | points.clear(); | |
| 41 | 81 | cells.clear(); | |
| 42 | 81 | _make_points(); | |
| 43 | 81 | _make_cells(); | |
| 44 | 81 | } | |
| 45 | |||
| 46 | private: | ||
| 47 | 81 | void _make_points() { | |
| 48 |
1/2✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
|
81 | const auto in_points = reader.points(); |
| 49 |
1/2✓ Branch 2 taken 81 times.
✗ Branch 3 not taken.
|
81 | const auto in_layout = in_points->layout(); |
| 50 |
1/2✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
|
81 | const auto in_np = in_layout.extent(0); |
| 51 |
3/6✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 81 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 81 times.
✗ Branch 7 not taken.
|
81 | const auto in_dim = in_layout.dimension() > 1 ? in_layout.extent(1) : 0; |
| 52 |
2/4✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 81 times.
|
81 | if (in_np != reader.number_of_points()) |
| 53 | ✗ | throw SizeError("Mismatch between stored and defined number of points."); | |
| 54 | |||
| 55 |
1/2✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
|
81 | points.reserve(in_np); |
| 56 |
1/2✓ Branch 2 taken 81 times.
✗ Branch 3 not taken.
|
243 | in_points->visit_field_values([&] <typename T> (std::span<const T> values) { |
| 57 |
2/4✓ Branch 1 taken 81 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 81 times.
✗ Branch 5 not taken.
|
25646 | std::ranges::for_each(std::views::iota(std::size_t{0}, in_np), [&] (auto p_idx) { |
| 58 |
1/24✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✗ Branch 37 not taken.
✗ Branch 38 not taken.
✓ Branch 41 taken 12742 times.
✗ Branch 42 not taken.
✗ Branch 45 not taken.
✗ Branch 46 not taken.
|
12742 | points.push_back({}); |
| 59 |
2/48✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 34 not taken.
✗ Branch 35 not taken.
✗ Branch 37 not taken.
✗ Branch 38 not taken.
✗ Branch 40 not taken.
✗ Branch 41 not taken.
✗ Branch 43 not taken.
✗ Branch 44 not taken.
✗ Branch 46 not taken.
✗ Branch 47 not taken.
✗ Branch 49 not taken.
✗ Branch 50 not taken.
✗ Branch 52 not taken.
✗ Branch 53 not taken.
✗ Branch 55 not taken.
✗ Branch 56 not taken.
✗ Branch 58 not taken.
✗ Branch 59 not taken.
✓ Branch 61 taken 12742 times.
✗ Branch 62 not taken.
✓ Branch 64 taken 12742 times.
✗ Branch 65 not taken.
✗ Branch 67 not taken.
✗ Branch 68 not taken.
✗ Branch 70 not taken.
✗ Branch 71 not taken.
|
89194 | std::ranges::for_each(std::views::iota(std::size_t{0}, in_dim), [&] (auto dim) { |
| 60 | 38226 | points.back().at(dim) = static_cast<double>(values[p_idx*in_dim + dim]); | |
| 61 | }); | ||
| 62 | }); | ||
| 63 | 162 | }); | |
| 64 | 81 | } | |
| 65 | |||
| 66 | 81 | void _make_cells() { | |
| 67 | 81 | cells.reserve(reader.number_of_cells()); | |
| 68 |
1/2✓ Branch 2 taken 81 times.
✗ Branch 3 not taken.
|
81 | reader.visit_cells([&] (CellType ct, std::vector<std::size_t> corners) { |
| 69 |
1/2✓ Branch 4 taken 10380 times.
✗ Branch 5 not taken.
|
10380 | cells.emplace_back(std::pair{std::move(ct), std::move(corners)}); |
| 70 | 10380 | }); | |
| 71 |
1/2✗ Branch 2 not taken.
✓ Branch 3 taken 81 times.
|
81 | if (cells.size() != reader.number_of_cells()) |
| 72 | ✗ | throw SizeError("Mismatch between stored and defined number of cells."); | |
| 73 | 81 | } | |
| 74 | }; | ||
| 75 | |||
| 76 | template<typename T> | ||
| 77 | concept Writer | ||
| 78 | = requires { typename std::remove_cvref_t<T>::Grid; } | ||
| 79 | and std::derived_from<std::remove_cvref_t<T>, GridWriterBase<typename T::Grid>>; | ||
| 80 | |||
| 81 | template<typename T> | ||
| 82 | concept PieceWriter = Writer<T> and std::derived_from<std::remove_cvref_t<T>, GridWriter<typename T::Grid>>; | ||
| 83 | |||
| 84 | template<typename T> | ||
| 85 | concept TimeSeriesWriter = Writer<T> and std::derived_from<std::remove_cvref_t<T>, TimeSeriesGridWriter<typename T::Grid>>; | ||
| 86 | |||
| 87 | template<typename T> | ||
| 88 | concept PieceWriterFactory = requires (const T& factory, const ConverterGrid& grid) { | ||
| 89 | { factory(grid) } -> PieceWriter; | ||
| 90 | }; | ||
| 91 | |||
| 92 | template<typename T> | ||
| 93 | concept TimeSeriesWriterFactory = requires (const T& factory, const ConverterGrid& grid, const std::string& filename) { | ||
| 94 | { factory(grid) } -> TimeSeriesWriter; | ||
| 95 | }; | ||
| 96 | |||
| 97 | template<typename T> | ||
| 98 | concept WriterFactory = PieceWriterFactory<T> or TimeSeriesWriterFactory<T>; | ||
| 99 | |||
| 100 | template<typename Reader, Writer Writer> | ||
| 101 | 166 | void add_piece_fields(const Reader& reader, Writer& writer) { | |
| 102 | 166 | writer.clear(); | |
| 103 |
6/10✓ Branch 1 taken 83 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 83 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 83 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 117 times.
✗ Branch 11 not taken.
✓ Branch 16 taken 117 times.
✓ Branch 17 taken 83 times.
|
400 | for (auto [name, field_ptr] : cell_fields(reader)) |
| 104 |
1/2✓ Branch 4 taken 117 times.
✗ Branch 5 not taken.
|
234 | writer.set_cell_field(std::move(name), std::move(field_ptr)); |
| 105 |
6/10✓ Branch 1 taken 83 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 83 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 83 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 186 times.
✗ Branch 11 not taken.
✓ Branch 16 taken 186 times.
✓ Branch 17 taken 83 times.
|
538 | for (auto [name, field_ptr] : point_fields(reader)) |
| 106 |
1/2✓ Branch 4 taken 186 times.
✗ Branch 5 not taken.
|
372 | writer.set_point_field(std::move(name), std::move(field_ptr)); |
| 107 |
4/10✓ Branch 1 taken 83 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 83 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 83 times.
✗ Branch 8 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 16 not taken.
✓ Branch 17 taken 83 times.
|
166 | for (auto [name, field_ptr] : meta_data_fields(reader)) |
| 108 | ✗ | writer.set_meta_data(std::move(name), std::move(field_ptr)); | |
| 109 | 166 | } | |
| 110 | |||
| 111 | template<typename Reader, PieceWriter Writer> | ||
| 112 | 44 | std::string write_piece(const Reader& reader, Writer& writer, const std::string& filename) { | |
| 113 | 44 | add_piece_fields(reader, writer); | |
| 114 | 44 | return writer.write(filename); | |
| 115 | } | ||
| 116 | |||
| 117 | template<typename Reader, TimeSeriesWriter Writer> | ||
| 118 | 105 | std::string write_piece(const Reader& reader, Writer& writer, double time_step) { | |
| 119 | 105 | add_piece_fields(reader, writer); | |
| 120 | 105 | return writer.write(time_step); | |
| 121 | } | ||
| 122 | |||
| 123 | } // namespace ConverterDetail | ||
| 124 | #endif // DOXYGEN | ||
| 125 | |||
| 126 | /*! | ||
| 127 | * \ingroup Grid | ||
| 128 | * \brief Convert between grid formats. | ||
| 129 | * \param reader A grid reader on which a file was opened. | ||
| 130 | * \param factory A factory to construct a writer with the desired output format. | ||
| 131 | * \param filename The name of the file to be written. | ||
| 132 | */ | ||
| 133 | template<std::derived_from<GridReader> Reader, ConverterDetail::WriterFactory Factory> | ||
| 134 | 44 | std::string convert(const Reader& reader, const std::string& filename, const Factory& factory) { | |
| 135 | 44 | ConverterDetail::ConverterGrid grid{reader}; | |
| 136 |
1/2✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
|
44 | auto writer = factory(grid); |
| 137 |
2/4✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 23 times.
|
44 | if (reader.filename() == filename + writer.extension()) |
| 138 | ✗ | throw GridFormat::IOError("Cannot read/write from/to the same file"); | |
| 139 | if constexpr (Traits::WritesConnectivity<std::remove_cvref_t<decltype(writer)>>::value) | ||
| 140 |
1/2✓ Branch 1 taken 21 times.
✗ Branch 2 not taken.
|
40 | grid.make_grid(); |
| 141 |
1/2✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
|
88 | return ConverterDetail::write_piece(reader, writer, filename); |
| 142 | 44 | } | |
| 143 | |||
| 144 | /*! | ||
| 145 | * \ingroup Grid | ||
| 146 | * \brief Overload for time series formats. | ||
| 147 | * \param reader A grid reader on which a file was opened. | ||
| 148 | * \param factory A factory to construct a time series writer with the desired output format. | ||
| 149 | * \param call_back (optional) A callback that is invoked after writing each step. | ||
| 150 | */ | ||
| 151 | template<std::derived_from<GridReader> Reader, | ||
| 152 | ConverterDetail::TimeSeriesWriterFactory Factory, | ||
| 153 | 15 | std::invocable<std::size_t, const std::string&> StepCallBack = decltype([] (std::size_t, const std::string&) {})> | |
| 154 | 21 | std::string convert(Reader& reader, | |
| 155 | const Factory& factory, | ||
| 156 | const StepCallBack& call_back = {}) { | ||
| 157 |
2/4✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
|
21 | if (!reader.is_sequence()) |
| 158 | ✗ | throw ValueError("Cannot convert data from reader to a sequence as the file read is no sequence."); | |
| 159 | |||
| 160 | 21 | ConverterDetail::ConverterGrid grid{reader}; | |
| 161 |
1/2✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
|
21 | auto writer = factory(grid); |
| 162 | 21 | std::string filename; | |
| 163 |
3/4✓ Branch 1 taken 72 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 60 times.
✓ Branch 4 taken 12 times.
|
126 | for (std::size_t step = 0; step < reader.number_of_steps(); ++step) { |
| 164 |
1/2✓ Branch 1 taken 60 times.
✗ Branch 2 not taken.
|
105 | reader.set_step(step); |
| 165 | if constexpr (Traits::WritesConnectivity<std::remove_cvref_t<decltype(writer)>>::value) | ||
| 166 |
1/2✓ Branch 1 taken 60 times.
✗ Branch 2 not taken.
|
105 | grid.make_grid(); |
| 167 |
2/4✓ Branch 1 taken 60 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 60 times.
✗ Branch 5 not taken.
|
105 | filename = ConverterDetail::write_piece(reader, writer, reader.time_at_step(step)); |
| 168 |
1/2✓ Branch 1 taken 45 times.
✗ Branch 2 not taken.
|
105 | call_back(step, filename); |
| 169 | } | ||
| 170 | 21 | return filename; | |
| 171 | 21 | } | |
| 172 | |||
| 173 | namespace Traits { | ||
| 174 | |||
| 175 | // to distinguish points/cells we use different integer types | ||
| 176 | |||
| 177 | template<> | ||
| 178 | struct Points<ConverterDetail::ConverterGrid> { | ||
| 179 | 162 | static std::ranges::range auto get(const ConverterDetail::ConverterGrid& grid) { | |
| 180 |
2/4✓ Branch 1 taken 162 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 162 times.
✗ Branch 5 not taken.
|
162 | return std::views::iota(std::size_t{0}, grid.reader.number_of_points()); |
| 181 | } | ||
| 182 | }; | ||
| 183 | |||
| 184 | template<> | ||
| 185 | struct Cells<ConverterDetail::ConverterGrid> { | ||
| 186 | 405 | static std::ranges::range auto get(const ConverterDetail::ConverterGrid& grid) { | |
| 187 |
1/2✓ Branch 1 taken 405 times.
✗ Branch 2 not taken.
|
405 | const auto max = static_cast<std::int64_t>(grid.reader.number_of_cells()); |
| 188 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 405 times.
|
405 | if (max < 0) |
| 189 | ✗ | throw TypeError("Integer overflow. Too many grid cells."); | |
| 190 |
1/2✓ Branch 1 taken 405 times.
✗ Branch 2 not taken.
|
405 | return std::views::iota(std::int64_t{0}, max); |
| 191 | } | ||
| 192 | }; | ||
| 193 | |||
| 194 | template<> | ||
| 195 | struct NumberOfPoints<ConverterDetail::ConverterGrid> { | ||
| 196 | 411 | static std::size_t get(const ConverterDetail::ConverterGrid& grid) { | |
| 197 | 411 | return grid.reader.number_of_points(); | |
| 198 | } | ||
| 199 | }; | ||
| 200 | |||
| 201 | template<> | ||
| 202 | struct NumberOfCells<ConverterDetail::ConverterGrid> { | ||
| 203 | 413 | static std::size_t get(const ConverterDetail::ConverterGrid& grid) { | |
| 204 | 413 | return grid.reader.number_of_cells(); | |
| 205 | } | ||
| 206 | }; | ||
| 207 | |||
| 208 | template<> | ||
| 209 | struct CellPoints<ConverterDetail::ConverterGrid, std::int64_t> { | ||
| 210 | 10380 | static std::ranges::range auto get(const ConverterDetail::ConverterGrid& grid, const std::int64_t i) { | |
| 211 | 10380 | return grid.cells.at(i).second | std::views::all; | |
| 212 | } | ||
| 213 | }; | ||
| 214 | |||
| 215 | template<> | ||
| 216 | struct CellType<ConverterDetail::ConverterGrid, std::int64_t> { | ||
| 217 | 10380 | static GridFormat::CellType get(const ConverterDetail::ConverterGrid& grid, const std::int64_t i) { | |
| 218 | 10380 | return grid.cells.at(i).first; | |
| 219 | } | ||
| 220 | }; | ||
| 221 | |||
| 222 | template<> | ||
| 223 | struct PointCoordinates<ConverterDetail::ConverterGrid, std::size_t> { | ||
| 224 | 12742 | static std::array<double, 3> get(const ConverterDetail::ConverterGrid& grid, const std::size_t i) { | |
| 225 | 12742 | return grid.points.at(i); | |
| 226 | } | ||
| 227 | }; | ||
| 228 | |||
| 229 | template<> | ||
| 230 | struct PointId<ConverterDetail::ConverterGrid, std::size_t> { | ||
| 231 | 70364 | static std::size_t get(const ConverterDetail::ConverterGrid&, const std::size_t i) { | |
| 232 | 70364 | return i; | |
| 233 | } | ||
| 234 | }; | ||
| 235 | |||
| 236 | template<> | ||
| 237 | struct NumberOfCellPoints<ConverterDetail::ConverterGrid, std::int64_t> { | ||
| 238 | 20760 | static std::size_t get(const ConverterDetail::ConverterGrid& grid, const std::int64_t i) { | |
| 239 | 20760 | return grid.cells.at(i).second.size(); | |
| 240 | } | ||
| 241 | }; | ||
| 242 | |||
| 243 | template<> | ||
| 244 | struct Origin<ConverterDetail::ConverterGrid> { | ||
| 245 | 2 | static std::array<double, 3> get(const ConverterDetail::ConverterGrid& grid) { | |
| 246 | 2 | return grid.reader.origin(); | |
| 247 | } | ||
| 248 | }; | ||
| 249 | |||
| 250 | template<> | ||
| 251 | struct Spacing<ConverterDetail::ConverterGrid> { | ||
| 252 | 2 | static std::array<double, 3> get(const ConverterDetail::ConverterGrid& grid) { | |
| 253 | 2 | return grid.reader.spacing(); | |
| 254 | } | ||
| 255 | }; | ||
| 256 | |||
| 257 | template<> | ||
| 258 | struct Basis<ConverterDetail::ConverterGrid> { | ||
| 259 | 2 | static std::array<std::array<double, 3>, 3> get(const ConverterDetail::ConverterGrid& grid) { | |
| 260 | std::array<std::array<double, 3>, 3> result; | ||
| 261 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
|
8 | for (unsigned int i = 0; i < 3; ++i) |
| 262 | 6 | result[i] = grid.reader.basis_vector(i); | |
| 263 | 2 | return result; | |
| 264 | } | ||
| 265 | }; | ||
| 266 | |||
| 267 | template<> | ||
| 268 | struct Extents<ConverterDetail::ConverterGrid> { | ||
| 269 | 3 | static std::array<std::size_t, 3> get(const ConverterDetail::ConverterGrid& grid) { | |
| 270 | 3 | return grid.reader.extents(); | |
| 271 | } | ||
| 272 | }; | ||
| 273 | |||
| 274 | template<> | ||
| 275 | struct Ordinates<ConverterDetail::ConverterGrid> { | ||
| 276 | static std::vector<double> get(const ConverterDetail::ConverterGrid& grid, unsigned int i) { | ||
| 277 | return grid.reader.ordinates(i); | ||
| 278 | } | ||
| 279 | }; | ||
| 280 | |||
| 281 | template<typename Entity> | ||
| 282 | struct Location<ConverterDetail::ConverterGrid, Entity> { | ||
| 283 | ✗ | static std::array<std::size_t, 3> get(const ConverterDetail::ConverterGrid& grid, const std::size_t point) { | |
| 284 | ✗ | return _get(Ranges::incremented(Extents<ConverterDetail::ConverterGrid>::get(grid), 1), point); | |
| 285 | } | ||
| 286 | |||
| 287 | ✗ | static std::array<std::size_t, 3> get(const ConverterDetail::ConverterGrid& grid, const std::int64_t cell) { | |
| 288 | ✗ | return _get(Extents<ConverterDetail::ConverterGrid>::get(grid), cell); | |
| 289 | } | ||
| 290 | |||
| 291 | private: | ||
| 292 | ✗ | static std::array<std::size_t, 3> _get(std::ranges::range auto extents, std::integral auto index) { | |
| 293 | // avoid zero extents | ||
| 294 | ✗ | std::ranges::for_each(extents, [] <std::integral T> (T& e) { e = std::max(e, T{1}); }); | |
| 295 | ✗ | const auto accumulate_until = [&] (int dim) { | |
| 296 | ✗ | auto range = extents | std::views::take(dim); | |
| 297 | ✗ | return std::accumulate( | |
| 298 | std::ranges::begin(range), | ||
| 299 | std::ranges::end(range), | ||
| 300 | std::size_t{1}, | ||
| 301 | std::multiplies{} | ||
| 302 | ✗ | ); | |
| 303 | }; | ||
| 304 | |||
| 305 | ✗ | const auto divisor1 = accumulate_until(1); | |
| 306 | ✗ | const auto divisor2 = accumulate_until(2); | |
| 307 | return { | ||
| 308 | ✗ | index%divisor2%divisor1, | |
| 309 | ✗ | index%divisor2/divisor1, | |
| 310 | ✗ | index/divisor2 | |
| 311 | ✗ | }; | |
| 312 | } | ||
| 313 | }; | ||
| 314 | |||
| 315 | } // namespace Traits | ||
| 316 | } // namespace GridFormat | ||
| 317 | |||
| 318 | #endif // GRIDFORMAT_GRID_CONVERTER_HPP_ | ||
| 319 |