prtkgaur commented on code in PR #48345:
URL: https://github.com/apache/arrow/pull/48345#discussion_r3920250395
##########
cpp/src/parquet/decoder.cc:
##########
@@ -2372,6 +2327,125 @@ class ByteStreamSplitDecoder<FLBAType> : public
ByteStreamSplitDecoderBase<FLBAT
}
};
+// ----------------------------------------------------------------------
+// ALP decoder (Adaptive Lossless floating-Point)
+
+template <typename DType>
+class AlpDecoder : public TypedDecoderImpl<DType> {
+ public:
+ using Base = TypedDecoderImpl<DType>;
+ using T = typename DType::c_type;
+
+ explicit AlpDecoder(const ColumnDescriptor* descr)
+ : Base(descr, Encoding::ALP), current_offset_{0}, needs_decode_{false} {
+ static_assert(std::is_same<T, float>::value || std::is_same<T,
double>::value,
+ "ALP only supports float and double types");
+ }
+
+ void SetData(int num_values, const uint8_t* data, int len) final {
+ Base::SetData(num_values, data, len);
+ current_offset_ = 0;
+ if (num_values > 0 && len <= 0) {
+ throw ParquetException("ALP SetData: num_values=" +
std::to_string(num_values) +
+ " but len=" + std::to_string(len));
+ }
+ needs_decode_ = (num_values > 0);
+ decoded_buffer_.clear();
+ }
+
+ int Decode(T* buffer, int max_values) override {
+ // Fast path: decode directly into output buffer if requesting all values
+ if (needs_decode_ && max_values >= this->num_values_) {
+ PARQUET_THROW_NOT_OK(::arrow::util::alp::AlpCodec<T>::Decode(
+ this->num_values_, this->data_, this->len_,
+ buffer));
+
+ const int decoded = this->num_values_;
+ this->num_values_ = 0;
+ needs_decode_ = false;
+ return decoded;
+ }
+
+ // Slow path: partial read - decode to intermediate buffer
+ // ALP Bit unpacker needs batches of 64
+ if (needs_decode_) {
+ decoded_buffer_.resize(this->num_values_);
+ PARQUET_THROW_NOT_OK(::arrow::util::alp::AlpCodec<T>::Decode(
+ this->num_values_, this->data_, this->len_,
+ decoded_buffer_.data()));
+ needs_decode_ = false;
+ }
+
+ // Copy from intermediate buffer
+ const int values_to_decode = std::min(
+ max_values,
+ static_cast<int>(decoded_buffer_.size() - current_offset_));
+
+ if (values_to_decode > 0) {
+ std::memcpy(buffer, decoded_buffer_.data() + current_offset_,
+ values_to_decode * sizeof(T));
+ current_offset_ += values_to_decode;
+ this->num_values_ -= values_to_decode;
+ }
+
+ return values_to_decode;
+ }
+
+ int DecodeArrow(int num_values, int null_count, const uint8_t* valid_bits,
+ int64_t valid_bits_offset,
+ typename EncodingTraits<DType>::Accumulator* builder)
override {
+ const int values_to_decode = num_values - null_count;
+ if (ARROW_PREDICT_FALSE(this->num_values_ < values_to_decode)) {
+ ParquetException::EofException("ALP DecodeArrow: Not enough values
available. "
Review Comment:
It throws. `ParquetException::EofException` is a `PARQUET_NORETURN static
void` helper that throws internally, so calling it as a statement is a throw —
which is why it reads like a plain call at the site. The `Status`-returning
paths around it go through `PARQUET_THROW_NOT_OK`.
##########
cpp/src/parquet/encoding_test.cc:
##########
@@ -2660,4 +2595,407 @@ TEST(DeltaByteArrayEncodingAdHoc, ArrowDirectPut) {
}
}
+// ----------------------------------------------------------------------
+// ALP encoding tests for float/double
+
+template <typename Type>
+class TestAlpEncoding : public TestEncodingBase<Type> {
+ public:
+ using c_type = typename Type::c_type;
+ static constexpr int TYPE = Type::type_num;
+ static constexpr size_t kNumRoundTrips = 3;
+
+ void CheckRoundtrip() override {
+ auto encoder =
+ MakeTypedEncoder<Type>(Encoding::ALP, /*use_dictionary=*/false,
descr_.get());
+ auto decoder = MakeTypedDecoder<Type>(Encoding::ALP, descr_.get());
+
+ for (size_t i = 0; i < kNumRoundTrips; ++i) {
+ encoder->Put(draws_, num_values_);
+ encode_buffer_ = encoder->FlushValues();
+
+ decoder->SetData(num_values_, encode_buffer_->data(),
+ static_cast<int>(encode_buffer_->size()));
+ int values_decoded = decoder->Decode(decode_buf_, num_values_);
+ ASSERT_EQ(num_values_, values_decoded);
+
+ // Use memcmp for bit-exact comparison (important for -0.0, NaN bit
patterns)
+ ASSERT_EQ(0, std::memcmp(draws_, decode_buf_, num_values_ *
sizeof(c_type)));
+ }
+ }
+
+ void CheckRoundtripSpaced(const uint8_t* valid_bits,
+ int64_t valid_bits_offset) override {
+ auto encoder =
+ MakeTypedEncoder<Type>(Encoding::ALP, /*use_dictionary=*/false,
descr_.get());
+ auto decoder = MakeTypedDecoder<Type>(Encoding::ALP, descr_.get());
+
+ int null_count = 0;
+ for (auto i = 0; i < num_values_; i++) {
+ if (!bit_util::GetBit(valid_bits, valid_bits_offset + i)) {
+ null_count++;
+ }
+ }
+
+ for (size_t i = 0; i < kNumRoundTrips; ++i) {
+ encoder->PutSpaced(draws_, num_values_, valid_bits, valid_bits_offset);
+ encode_buffer_ = encoder->FlushValues();
+
+ decoder->SetData(num_values_ - null_count, encode_buffer_->data(),
+ static_cast<int>(encode_buffer_->size()));
+ auto values_decoded = decoder->DecodeSpaced(decode_buf_, num_values_,
null_count,
+ valid_bits,
valid_bits_offset);
+ ASSERT_EQ(num_values_, values_decoded);
+
+ // Verify only valid values
+ for (int j = 0; j < num_values_; ++j) {
+ if (bit_util::GetBit(valid_bits, valid_bits_offset + j)) {
+ ASSERT_EQ(0, std::memcmp(&draws_[j], &decode_buf_[j],
sizeof(c_type))) << j;
+ }
+ }
+ }
+ }
+
+ void InitDataWithSpecialValues(int nvalues, int repeats) {
+ num_values_ = nvalues * repeats;
+ this->input_bytes_.resize(num_values_ * sizeof(c_type));
+ this->output_bytes_.resize(num_values_ * sizeof(c_type));
+ draws_ = reinterpret_cast<c_type*>(this->input_bytes_.data());
+ decode_buf_ = reinterpret_cast<c_type*>(this->output_bytes_.data());
+
+ // Fill with mix of normal and special values
+ for (int i = 0; i < nvalues; ++i) {
+ if (i % 20 == 0) {
+ draws_[i] = std::numeric_limits<c_type>::quiet_NaN();
+ } else if (i % 20 == 5) {
+ draws_[i] = std::numeric_limits<c_type>::infinity();
+ } else if (i % 20 == 10) {
+ draws_[i] = -std::numeric_limits<c_type>::infinity();
+ } else if (i % 20 == 15) {
+ draws_[i] = static_cast<c_type>(-0.0);
+ } else {
+ draws_[i] = static_cast<c_type>(i) * static_cast<c_type>(0.123);
+ }
+ }
+
+ // Repeat pattern
+ for (int j = 1; j < repeats; ++j) {
+ for (int i = 0; i < nvalues; ++i) {
+ draws_[nvalues * j + i] = draws_[i];
+ }
+ }
+ }
+
+ void InitDataDecimalPattern(int nvalues, int repeats) {
+ num_values_ = nvalues * repeats;
+ this->input_bytes_.resize(num_values_ * sizeof(c_type));
+ this->output_bytes_.resize(num_values_ * sizeof(c_type));
+ draws_ = reinterpret_cast<c_type*>(this->input_bytes_.data());
+ decode_buf_ = reinterpret_cast<c_type*>(this->output_bytes_.data());
+
+ // Decimal-like values that ALP compresses well
+ for (int i = 0; i < nvalues; ++i) {
+ draws_[i] = static_cast<c_type>(100.0 + i * 0.01);
+ }
+
+ for (int j = 1; j < repeats; ++j) {
+ for (int i = 0; i < nvalues; ++i) {
+ draws_[nvalues * j + i] = draws_[i];
+ }
+ }
+ }
+
+ void ExecuteSpecialValues(int nvalues, int repeats) {
+ InitDataWithSpecialValues(nvalues, repeats);
+ CheckRoundtrip();
+ }
+
+ void ExecuteDecimalPattern(int nvalues, int repeats) {
+ InitDataDecimalPattern(nvalues, repeats);
+ CheckRoundtrip();
+ }
+
+ protected:
+ USING_BASE_MEMBERS();
+};
+
+using AlpEncodedTypes = ::testing::Types<FloatType, DoubleType>;
+TYPED_TEST_SUITE(TestAlpEncoding, AlpEncodedTypes);
+
+TYPED_TEST(TestAlpEncoding, BasicRoundTrip) {
+ // Test various sizes including edge cases
+ for (int values = 1; values < 32; ++values) {
+ ASSERT_NO_FATAL_FAILURE(this->Execute(values, 1));
+ }
+
+ // Test exactly vector size (1024)
+ ASSERT_NO_FATAL_FAILURE(this->Execute(1024, 1));
+
+ // Test just under and over vector size
+ ASSERT_NO_FATAL_FAILURE(this->Execute(1023, 1));
+ ASSERT_NO_FATAL_FAILURE(this->Execute(1025, 1));
+
+ // Test multiple vectors
+ ASSERT_NO_FATAL_FAILURE(this->Execute(2048, 1));
+ ASSERT_NO_FATAL_FAILURE(this->Execute(3000, 1));
+}
+
+TYPED_TEST(TestAlpEncoding, RoundTripWithRepeats) {
+ // Test with repeated patterns
+ ASSERT_NO_FATAL_FAILURE(this->Execute(100, 10));
+ ASSERT_NO_FATAL_FAILURE(this->Execute(1024, 3));
+}
+
+TYPED_TEST(TestAlpEncoding, SpecialValues) {
+ // Test NaN, Inf, -Inf, -0.0 (these become exceptions in ALP)
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpecialValues(100, 1));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpecialValues(1024, 1));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpecialValues(2000, 1));
+}
+
+TYPED_TEST(TestAlpEncoding, DecimalPatterns) {
+ // Test decimal-like values that ALP compresses efficiently
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteDecimalPattern(100, 1));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteDecimalPattern(1024, 1));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteDecimalPattern(5000, 1));
+}
+
+TYPED_TEST(TestAlpEncoding, SpacedRoundTrip) {
+ // Test with null values at various probabilities
+ for (double null_prob : {0.0, 0.1, 0.5, 0.9}) {
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(100, 1, 0, null_prob));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(1024, 1, 0, null_prob));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(2000, 1, 0, null_prob));
+ }
+
+ // Test with offset
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(1024, 1, 7, 0.3));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(1024, 1, 64, 0.5));
+}
+
+TYPED_TEST(TestAlpEncoding, LargeDataset) {
+ // Test with large dataset (multiple pages worth)
+ ASSERT_NO_FATAL_FAILURE(this->Execute(100000, 1));
+}
+
+TYPED_TEST(TestAlpEncoding, RandomData) {
+ using c_type = typename TypeParam::c_type;
+ ::arrow::random::RandomArrayGenerator rag(42);
+
+ // Generate random float/double array
+ std::shared_ptr<::arrow::Array> arr;
+ if constexpr (std::is_same_v<c_type, float>) {
+ arr = rag.Float32(10000, -1000.0f, 1000.0f);
+ } else {
+ arr = rag.Float64(10000, -1000.0, 1000.0);
+ }
+
+ auto encoder = MakeTypedEncoder<TypeParam>(Encoding::ALP, false,
this->descr_.get());
+ ASSERT_NO_THROW(encoder->Put(*arr));
+ auto buffer = encoder->FlushValues();
+
+ auto decoder = MakeTypedDecoder<TypeParam>(Encoding::ALP,
this->descr_.get());
+ decoder->SetData(static_cast<int>(arr->length()), buffer->data(),
+ static_cast<int>(buffer->size()));
+
+ std::vector<c_type> output(arr->length());
+ int decoded = decoder->Decode(output.data(),
static_cast<int>(arr->length()));
+ ASSERT_EQ(decoded, arr->length());
+
+ // Verify round-trip
+ auto typed_arr = std::static_pointer_cast<
+ typename std::conditional<std::is_same_v<c_type, float>,
+ ::arrow::FloatArray,
::arrow::DoubleArray>::type>(arr);
+ ASSERT_EQ(0, std::memcmp(output.data(), typed_arr->raw_values(),
+ arr->length() * sizeof(c_type)));
+}
+
+TEST(AlpEncodingAdHoc, InvalidDataTypes) {
+ // ALP only supports float and double
+ ASSERT_THROW(MakeTypedEncoder<Int32Type>(Encoding::ALP), ParquetException);
+ ASSERT_THROW(MakeTypedEncoder<Int64Type>(Encoding::ALP), ParquetException);
+ ASSERT_THROW(MakeTypedEncoder<BooleanType>(Encoding::ALP), ParquetException);
+ ASSERT_THROW(MakeTypedEncoder<ByteArrayType>(Encoding::ALP),
ParquetException);
+
+ ASSERT_THROW(MakeTypedDecoder<Int32Type>(Encoding::ALP), ParquetException);
+ ASSERT_THROW(MakeTypedDecoder<Int64Type>(Encoding::ALP), ParquetException);
+ ASSERT_THROW(MakeTypedDecoder<BooleanType>(Encoding::ALP), ParquetException);
+ ASSERT_THROW(MakeTypedDecoder<ByteArrayType>(Encoding::ALP),
ParquetException);
+}
+
+TEST(AlpEncodingAdHoc, ConstantValues) {
Review Comment:
Folded — it's `TYPED_TEST(TestAlpEncoding, ConstantValues)` over float and
double now. The one plain `TEST` left is `InvalidDataTypes`, which asserts the
non-float types are rejected, so it can't be parameterized over the float types.
##########
cpp/src/parquet/encoding_test.cc:
##########
@@ -2660,4 +2595,407 @@ TEST(DeltaByteArrayEncodingAdHoc, ArrowDirectPut) {
}
}
+// ----------------------------------------------------------------------
+// ALP encoding tests for float/double
+
+template <typename Type>
+class TestAlpEncoding : public TestEncodingBase<Type> {
+ public:
+ using c_type = typename Type::c_type;
+ static constexpr int TYPE = Type::type_num;
+ static constexpr size_t kNumRoundTrips = 3;
+
+ void CheckRoundtrip() override {
+ auto encoder =
+ MakeTypedEncoder<Type>(Encoding::ALP, /*use_dictionary=*/false,
descr_.get());
+ auto decoder = MakeTypedDecoder<Type>(Encoding::ALP, descr_.get());
+
+ for (size_t i = 0; i < kNumRoundTrips; ++i) {
+ encoder->Put(draws_, num_values_);
+ encode_buffer_ = encoder->FlushValues();
+
+ decoder->SetData(num_values_, encode_buffer_->data(),
+ static_cast<int>(encode_buffer_->size()));
+ int values_decoded = decoder->Decode(decode_buf_, num_values_);
+ ASSERT_EQ(num_values_, values_decoded);
+
+ // Use memcmp for bit-exact comparison (important for -0.0, NaN bit
patterns)
+ ASSERT_EQ(0, std::memcmp(draws_, decode_buf_, num_values_ *
sizeof(c_type)));
+ }
+ }
+
+ void CheckRoundtripSpaced(const uint8_t* valid_bits,
+ int64_t valid_bits_offset) override {
+ auto encoder =
+ MakeTypedEncoder<Type>(Encoding::ALP, /*use_dictionary=*/false,
descr_.get());
+ auto decoder = MakeTypedDecoder<Type>(Encoding::ALP, descr_.get());
+
+ int null_count = 0;
+ for (auto i = 0; i < num_values_; i++) {
+ if (!bit_util::GetBit(valid_bits, valid_bits_offset + i)) {
+ null_count++;
+ }
+ }
+
+ for (size_t i = 0; i < kNumRoundTrips; ++i) {
+ encoder->PutSpaced(draws_, num_values_, valid_bits, valid_bits_offset);
+ encode_buffer_ = encoder->FlushValues();
+
+ decoder->SetData(num_values_ - null_count, encode_buffer_->data(),
+ static_cast<int>(encode_buffer_->size()));
+ auto values_decoded = decoder->DecodeSpaced(decode_buf_, num_values_,
null_count,
+ valid_bits,
valid_bits_offset);
+ ASSERT_EQ(num_values_, values_decoded);
+
+ // Verify only valid values
+ for (int j = 0; j < num_values_; ++j) {
+ if (bit_util::GetBit(valid_bits, valid_bits_offset + j)) {
+ ASSERT_EQ(0, std::memcmp(&draws_[j], &decode_buf_[j],
sizeof(c_type))) << j;
+ }
+ }
+ }
+ }
+
+ void InitDataWithSpecialValues(int nvalues, int repeats) {
+ num_values_ = nvalues * repeats;
+ this->input_bytes_.resize(num_values_ * sizeof(c_type));
+ this->output_bytes_.resize(num_values_ * sizeof(c_type));
+ draws_ = reinterpret_cast<c_type*>(this->input_bytes_.data());
+ decode_buf_ = reinterpret_cast<c_type*>(this->output_bytes_.data());
+
+ // Fill with mix of normal and special values
+ for (int i = 0; i < nvalues; ++i) {
+ if (i % 20 == 0) {
+ draws_[i] = std::numeric_limits<c_type>::quiet_NaN();
+ } else if (i % 20 == 5) {
+ draws_[i] = std::numeric_limits<c_type>::infinity();
+ } else if (i % 20 == 10) {
+ draws_[i] = -std::numeric_limits<c_type>::infinity();
+ } else if (i % 20 == 15) {
+ draws_[i] = static_cast<c_type>(-0.0);
+ } else {
+ draws_[i] = static_cast<c_type>(i) * static_cast<c_type>(0.123);
+ }
+ }
+
+ // Repeat pattern
+ for (int j = 1; j < repeats; ++j) {
+ for (int i = 0; i < nvalues; ++i) {
+ draws_[nvalues * j + i] = draws_[i];
+ }
+ }
+ }
+
+ void InitDataDecimalPattern(int nvalues, int repeats) {
+ num_values_ = nvalues * repeats;
+ this->input_bytes_.resize(num_values_ * sizeof(c_type));
+ this->output_bytes_.resize(num_values_ * sizeof(c_type));
+ draws_ = reinterpret_cast<c_type*>(this->input_bytes_.data());
+ decode_buf_ = reinterpret_cast<c_type*>(this->output_bytes_.data());
+
+ // Decimal-like values that ALP compresses well
+ for (int i = 0; i < nvalues; ++i) {
+ draws_[i] = static_cast<c_type>(100.0 + i * 0.01);
+ }
+
+ for (int j = 1; j < repeats; ++j) {
+ for (int i = 0; i < nvalues; ++i) {
+ draws_[nvalues * j + i] = draws_[i];
+ }
+ }
+ }
+
+ void ExecuteSpecialValues(int nvalues, int repeats) {
+ InitDataWithSpecialValues(nvalues, repeats);
+ CheckRoundtrip();
+ }
+
+ void ExecuteDecimalPattern(int nvalues, int repeats) {
+ InitDataDecimalPattern(nvalues, repeats);
+ CheckRoundtrip();
+ }
+
+ protected:
+ USING_BASE_MEMBERS();
+};
+
+using AlpEncodedTypes = ::testing::Types<FloatType, DoubleType>;
+TYPED_TEST_SUITE(TestAlpEncoding, AlpEncodedTypes);
+
+TYPED_TEST(TestAlpEncoding, BasicRoundTrip) {
+ // Test various sizes including edge cases
+ for (int values = 1; values < 32; ++values) {
+ ASSERT_NO_FATAL_FAILURE(this->Execute(values, 1));
+ }
+
+ // Test exactly vector size (1024)
+ ASSERT_NO_FATAL_FAILURE(this->Execute(1024, 1));
+
+ // Test just under and over vector size
+ ASSERT_NO_FATAL_FAILURE(this->Execute(1023, 1));
+ ASSERT_NO_FATAL_FAILURE(this->Execute(1025, 1));
+
+ // Test multiple vectors
+ ASSERT_NO_FATAL_FAILURE(this->Execute(2048, 1));
+ ASSERT_NO_FATAL_FAILURE(this->Execute(3000, 1));
+}
+
+TYPED_TEST(TestAlpEncoding, RoundTripWithRepeats) {
+ // Test with repeated patterns
+ ASSERT_NO_FATAL_FAILURE(this->Execute(100, 10));
+ ASSERT_NO_FATAL_FAILURE(this->Execute(1024, 3));
+}
+
+TYPED_TEST(TestAlpEncoding, SpecialValues) {
+ // Test NaN, Inf, -Inf, -0.0 (these become exceptions in ALP)
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpecialValues(100, 1));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpecialValues(1024, 1));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpecialValues(2000, 1));
+}
+
+TYPED_TEST(TestAlpEncoding, DecimalPatterns) {
+ // Test decimal-like values that ALP compresses efficiently
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteDecimalPattern(100, 1));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteDecimalPattern(1024, 1));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteDecimalPattern(5000, 1));
+}
+
+TYPED_TEST(TestAlpEncoding, SpacedRoundTrip) {
+ // Test with null values at various probabilities
+ for (double null_prob : {0.0, 0.1, 0.5, 0.9}) {
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(100, 1, 0, null_prob));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(1024, 1, 0, null_prob));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(2000, 1, 0, null_prob));
+ }
+
+ // Test with offset
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(1024, 1, 7, 0.3));
+ ASSERT_NO_FATAL_FAILURE(this->ExecuteSpaced(1024, 1, 64, 0.5));
+}
+
+TYPED_TEST(TestAlpEncoding, LargeDataset) {
+ // Test with large dataset (multiple pages worth)
+ ASSERT_NO_FATAL_FAILURE(this->Execute(100000, 1));
+}
+
+TYPED_TEST(TestAlpEncoding, RandomData) {
+ using c_type = typename TypeParam::c_type;
+ ::arrow::random::RandomArrayGenerator rag(42);
+
+ // Generate random float/double array
+ std::shared_ptr<::arrow::Array> arr;
+ if constexpr (std::is_same_v<c_type, float>) {
+ arr = rag.Float32(10000, -1000.0f, 1000.0f);
Review Comment:
Widened — that generator spans ±1e30 now instead of ±1000, so values land on
both sides of the ALP encodable window and the exception fallback actually gets
exercised. Beyond that there are `SpecialValues`, `ConstantValues`,
`AllExceptions` and `BoundaryValues`, plus `RandomAndExtremes` pushing on the
encodable/exception boundary — all parameterized over float and double.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]