18 #include "absl/status/status.h" 19 #include "absl/status/statusor.h" 20 #include "absl/strings/match.h" 21 #include "absl/strings/str_split.h" 35 absl::Status
ParseFile(
const std::string& file_name, Data* data,
40 static const int kNumFields;
43 static const int kFieldStartPos[];
46 static const int kFieldLength[];
49 static const int kSpacePos[];
55 void DisplaySummary();
58 absl::Status SplitLineIntoFields();
64 std::string GetFirstWord()
const;
68 bool IsCommentOrBlank()
const;
71 const std::string& GetField(
int offset,
int index)
const {
72 return fields_[offset +
index];
80 int GetFieldOffset()
const {
return free_form_ ? fields_.size() & 1 : 0; }
83 template <
class DataWrapper>
84 absl::Status ProcessLine(
const std::string& line,
DataWrapper* data);
87 template <
class DataWrapper>
88 absl::Status ProcessObjectiveSenseSection(
DataWrapper* data);
91 template <
class DataWrapper>
92 absl::Status ProcessRowsSection(
bool is_lazy,
DataWrapper* data);
95 template <
class DataWrapper>
96 absl::Status ProcessColumnsSection(
DataWrapper* data);
99 template <
class DataWrapper>
103 template <
class DataWrapper>
104 absl::Status ProcessRangesSection(
DataWrapper* data);
107 template <
class DataWrapper>
108 absl::Status ProcessBoundsSection(
DataWrapper* data);
111 template <
class DataWrapper>
112 absl::Status ProcessIndicatorsSection(
DataWrapper* data);
115 absl::Status ProcessSosSection();
119 absl::StatusOr<double> GetDoubleFromString(
const std::string& str);
120 absl::StatusOr<bool> GetBoolFromString(
const std::string& str);
130 INFINITE_LOWER_BOUND,
131 INFINITE_UPPER_BOUND,
146 template <
class DataWrapper>
147 absl::Status StoreBound(
const std::string& bound_type_mnemonic,
148 const std::string& column_name,
149 const std::string& bound_value,
DataWrapper* data);
152 template <
class DataWrapper>
153 absl::Status StoreCoefficient(
int col,
const std::string& row_name,
154 const std::string& row_value,
158 template <
class DataWrapper>
159 absl::Status StoreRightHandSide(
const std::string& row_name,
160 const std::string& row_value,
164 template <
class DataWrapper>
165 absl::Status StoreRange(
const std::string& row_name,
166 const std::string& range_value,
DataWrapper* data);
170 absl::Status InvalidArgumentError(
const std::string& error_message);
174 absl::Status AppendLineToError(
const absl::Status& status);
180 std::vector<std::string> fields_;
183 std::string objective_name_;
206 absl::flat_hash_map<std::string, SectionId> section_name_to_id_map_;
209 absl::flat_hash_map<std::string, RowTypeId> row_name_to_id_map_;
212 absl::flat_hash_map<std::string, BoundTypeId> bound_name_to_id_map_;
215 absl::flat_hash_set<std::string> integer_type_names_set_;
225 std::vector<bool> is_binary_by_default_;
230 bool in_integer_section_;
235 int num_unconstrained_rows_;
242 template <
class Data>
251 data_->SetDcheckBounds(
false);
258 data_->SetMaximizationProblem(maximize);
262 return data_->FindOrCreateConstraint(
name).value();
269 data_->SetCoefficient(RowIndex(row_index), ColIndex(col_index),
274 <<
"LAZYCONS section detected. It will be handled as an extension of " 278 return data_->constraint_lower_bounds()[RowIndex(row_index)];
281 return data_->constraint_upper_bounds()[RowIndex(row_index)];
285 return data_->FindOrCreateVariable(
name).value();
288 data_->SetVariableType(ColIndex(
index),
298 return data_->IsVariableInteger(ColIndex(
index));
301 return data_->variable_lower_bounds()[ColIndex(
index)];
304 return data_->variable_upper_bounds()[ColIndex(
index)];
309 return absl::UnimplementedError(
310 "LinearProgram does not support indicator constraints.");
331 const auto it = constraint_indices_by_name_.find(
name);
332 if (it != constraint_indices_by_name_.end())
return it->second;
334 const int index = data_->constraint_size();
339 constraint_indices_by_name_[
name] =
index;
357 data_->mutable_constraint(row_index)->set_is_lazy(
true);
360 return data_->constraint(row_index).lower_bound();
363 return data_->constraint(row_index).upper_bound();
367 const auto it = variable_indices_by_name_.find(
name);
368 if (it != variable_indices_by_name_.end())
return it->second;
370 const int index = data_->variable_size();
378 data_->mutable_variable(
index)->set_is_integer(
true);
388 return data_->variable(
index).is_integer();
391 return data_->variable(
index).lower_bound();
394 return data_->variable(
index).upper_bound();
399 const auto it = constraint_indices_by_name_.find(cst_name);
400 if (it == constraint_indices_by_name_.end()) {
401 return absl::InvalidArgumentError(
402 absl::StrCat(
"Constraint \"", cst_name,
"\" doesn't exist."));
404 const int cst_index = it->second;
407 data_->add_general_constraint();
409 absl::StrCat(
"ind_", data_->constraint(cst_index).name()));
415 constraints_to_delete_.insert(cst_index);
417 return absl::OkStatus();
422 constraints_to_delete_);
428 absl::flat_hash_map<std::string, int> variable_indices_by_name_;
429 absl::flat_hash_map<std::string, int> constraint_indices_by_name_;
430 absl::node_hash_set<int> constraints_to_delete_;
433 template <
class Data>
436 if (data ==
nullptr) {
437 return absl::InvalidArgumentError(
"NULL pointer passed as argument.");
442 return absl::OkStatus();
451 data_wrapper.SetUp();
452 for (
const std::string& line :
456 data_wrapper.CleanUp();
458 return absl::OkStatus();
461 template <
class DataWrapper>
462 absl::Status MPSReaderImpl::ProcessLine(
const std::string& line,
466 if (IsCommentOrBlank()) {
467 return absl::OkStatus();
469 if (!free_form_ && absl::StrContains(line_,
'\t')) {
470 return InvalidArgumentError(
"File contains tabs.");
473 if (line[0] !=
'\0' && line[0] !=
' ') {
474 section = GetFirstWord();
477 if (section_ == UNKNOWN_SECTION) {
478 return InvalidArgumentError(
"Unknown section.");
480 if (section_ == COMMENT) {
481 return absl::OkStatus();
483 if (section_ == OBJSENSE) {
484 return absl::OkStatus();
486 if (section_ == NAME) {
495 if (fields_.size() >= 2) {
496 data->SetName(fields_[1]);
499 const std::vector<std::string> free_fields =
500 absl::StrSplit(line_, absl::ByAnyChar(
" \t"), absl::SkipEmpty());
501 const std::string free_name =
502 free_fields.size() >= 2 ? free_fields[1] :
"";
503 const std::string fixed_name = fields_.size() >= 3 ? fields_[2] :
"";
504 if (free_name != fixed_name) {
505 return InvalidArgumentError(
506 "Fixed form invalid: name differs between free and fixed " 509 data->SetName(fixed_name);
512 return absl::OkStatus();
517 return InvalidArgumentError(
"Second NAME field.");
519 return ProcessObjectiveSenseSection(data);
521 return ProcessRowsSection(
false, data);
523 return ProcessRowsSection(
true, data);
525 return ProcessColumnsSection(data);
527 return ProcessRhsSection(data);
529 return ProcessRangesSection(data);
531 return ProcessBoundsSection(data);
533 return ProcessIndicatorsSection(data);
535 return ProcessSosSection();
539 return InvalidArgumentError(
"Unknown section.");
541 return absl::OkStatus();
544 template <
class DataWrapper>
545 absl::Status MPSReaderImpl::ProcessObjectiveSenseSection(DataWrapper* data) {
546 if (fields_.size() != 1 && fields_[0] !=
"MIN" && fields_[0] !=
"MAX") {
547 return InvalidArgumentError(
"Expected objective sense (MAX or MIN).");
549 data->SetObjectiveDirection(fields_[0] ==
"MAX");
550 return absl::OkStatus();
553 template <
class DataWrapper>
554 absl::Status MPSReaderImpl::ProcessRowsSection(
bool is_lazy,
556 if (fields_.size() < 2) {
557 return InvalidArgumentError(
"Not enough fields in ROWS section.");
559 const std::string row_type_name = fields_[0];
560 const std::string row_name = fields_[1];
563 if (row_type == UNKNOWN_ROW_TYPE) {
564 return InvalidArgumentError(
"Unknown row type.");
568 if (objective_name_.empty() && row_type == NONE) {
569 row_type = OBJECTIVE;
570 objective_name_ = row_name;
572 if (row_type == NONE) {
573 ++num_unconstrained_rows_;
575 const int row = data->FindOrCreateConstraint(row_name);
576 if (is_lazy) data->SetIsLazy(
row);
583 data->ConstraintUpperBound(
row));
586 data->SetConstraintBounds(
row, data->ConstraintLowerBound(
row),
597 return absl::OkStatus();
600 template <
class DataWrapper>
601 absl::Status MPSReaderImpl::ProcessColumnsSection(DataWrapper* data) {
603 if (absl::StrContains(line_,
"'MARKER'")) {
604 if (absl::StrContains(line_,
"'INTORG'")) {
605 VLOG(2) <<
"Entering integer marker.\n" << line_;
606 if (in_integer_section_) {
607 return InvalidArgumentError(
"Found INTORG inside the integer section.");
609 in_integer_section_ =
true;
610 }
else if (absl::StrContains(line_,
"'INTEND'")) {
611 VLOG(2) <<
"Leaving integer marker.\n" << line_;
612 if (!in_integer_section_) {
613 return InvalidArgumentError(
614 "Found INTEND without corresponding INTORG.");
616 in_integer_section_ =
false;
618 return absl::OkStatus();
620 const int start_index = free_form_ ? 0 : 1;
621 if (fields_.size() < start_index + 3) {
622 return InvalidArgumentError(
"Not enough fields in COLUMNS section.");
624 const std::string& column_name = GetField(start_index, 0);
625 const std::string& row1_name = GetField(start_index, 1);
626 const std::string& row1_value = GetField(start_index, 2);
627 const int col = data->FindOrCreateVariable(column_name);
628 is_binary_by_default_.resize(
col + 1,
false);
629 if (in_integer_section_) {
630 data->SetVariableTypeToInteger(
col);
632 data->SetVariableBounds(
col, 0.0, 1.0);
633 is_binary_by_default_[
col] =
true;
638 if (fields_.size() == start_index + 4) {
639 return InvalidArgumentError(
"Unexpected number of fields.");
641 if (fields_.size() - start_index > 4) {
642 const std::string& row2_name = GetField(start_index, 3);
643 const std::string& row2_value = GetField(start_index, 4);
646 return absl::OkStatus();
649 template <
class DataWrapper>
650 absl::Status MPSReaderImpl::ProcessRhsSection(DataWrapper* data) {
651 const int start_index = free_form_ ? 0 : 2;
652 const int offset = start_index + GetFieldOffset();
653 if (fields_.size() < offset + 2) {
654 return InvalidArgumentError(
"Not enough fields in RHS section.");
657 const std::string& row1_name = GetField(offset, 0);
658 const std::string& row1_value = GetField(offset, 1);
660 if (fields_.size() - start_index >= 4) {
661 const std::string& row2_name = GetField(offset, 2);
662 const std::string& row2_value = GetField(offset, 3);
665 return absl::OkStatus();
668 template <
class DataWrapper>
669 absl::Status MPSReaderImpl::ProcessRangesSection(DataWrapper* data) {
670 const int start_index = free_form_ ? 0 : 2;
671 const int offset = start_index + GetFieldOffset();
672 if (fields_.size() < offset + 2) {
673 return InvalidArgumentError(
"Not enough fields in RHS section.");
676 const std::string& row1_name = GetField(offset, 0);
677 const std::string& row1_value = GetField(offset, 1);
679 if (fields_.size() - start_index >= 4) {
680 const std::string& row2_name = GetField(offset, 2);
681 const std::string& row2_value = GetField(offset, 3);
684 return absl::OkStatus();
687 template <
class DataWrapper>
688 absl::Status MPSReaderImpl::ProcessBoundsSection(DataWrapper* data) {
689 if (fields_.size() < 3) {
690 return InvalidArgumentError(
"Not enough fields in BOUNDS section.");
692 const std::string bound_type_mnemonic = fields_[0];
693 const std::string bound_row_name = fields_[1];
694 const std::string column_name = fields_[2];
695 std::string bound_value;
696 if (fields_.size() >= 4) {
697 bound_value = fields_[3];
699 return StoreBound(bound_type_mnemonic, column_name, bound_value, data);
702 template <
class DataWrapper>
703 absl::Status MPSReaderImpl::ProcessIndicatorsSection(DataWrapper* data) {
707 if (fields_.size() < 4) {
708 return InvalidArgumentError(
"Not enough fields in INDICATORS section.");
711 const std::string type = fields_[0];
713 return InvalidArgumentError(
714 "Indicator constraints must start with \"IF\".");
716 const std::string row_name = fields_[1];
717 const std::string column_name = fields_[2];
718 const std::string column_value = fields_[3];
723 const int col = data->FindOrCreateVariable(column_name);
725 data->SetVariableTypeToInteger(
col);
726 data->SetVariableBounds(
col,
std::max(0.0, data->VariableLowerBound(
col)),
730 AppendLineToError(data->CreateIndicatorConstraint(row_name,
col,
value)));
732 return absl::OkStatus();
735 template <
class DataWrapper>
736 absl::Status MPSReaderImpl::StoreCoefficient(
int col,
737 const std::string& row_name,
738 const std::string& row_value,
740 if (row_name.empty() || row_name ==
"$") {
741 return absl::OkStatus();
747 return InvalidArgumentError(
"Constraint coefficients cannot be infinity.");
749 if (
value == 0.0)
return absl::OkStatus();
750 if (row_name == objective_name_) {
751 data->SetObjectiveCoefficient(
col,
value);
753 const int row = data->FindOrCreateConstraint(row_name);
756 return absl::OkStatus();
759 template <
class DataWrapper>
760 absl::Status MPSReaderImpl::StoreRightHandSide(
const std::string& row_name,
761 const std::string& row_value,
763 if (row_name.empty())
return absl::OkStatus();
765 if (row_name != objective_name_) {
766 const int row = data->FindOrCreateConstraint(row_name);
779 return absl::OkStatus();
782 template <
class DataWrapper>
783 absl::Status MPSReaderImpl::StoreRange(
const std::string& row_name,
784 const std::string& range_value,
786 if (row_name.empty())
return absl::OkStatus();
788 const int row = data->FindOrCreateConstraint(row_name);
808 return absl::OkStatus();
811 template <
class DataWrapper>
812 absl::Status MPSReaderImpl::StoreBound(
const std::string& bound_type_mnemonic,
813 const std::string& column_name,
814 const std::string& bound_value,
817 bound_name_to_id_map_, bound_type_mnemonic, UNKNOWN_BOUND_TYPE);
818 if (bound_type_id == UNKNOWN_BOUND_TYPE) {
819 return InvalidArgumentError(
"Unknown bound type.");
821 const int col = data->FindOrCreateVariable(column_name);
822 if (integer_type_names_set_.count(bound_type_mnemonic) != 0) {
823 data->SetVariableTypeToInteger(
col);
825 if (is_binary_by_default_.size() <=
col) {
827 is_binary_by_default_.resize(
col + 1,
false);
830 DCHECK(!is_binary_by_default_[
col] || data->VariableIsInteger(
col));
836 if (is_binary_by_default_[
col]) {
840 switch (bound_type_id) {
844 if (bound_type_mnemonic ==
"LI" &&
lower_bound == 0.0) {
853 case FIXED_VARIABLE: {
862 case INFINITE_LOWER_BOUND:
865 case INFINITE_UPPER_BOUND:
872 case UNKNOWN_BOUND_TYPE:
874 return InvalidArgumentError(
"Unknown bound type.");
876 is_binary_by_default_[
col] =
false;
878 return absl::OkStatus();
881 const int MPSReaderImpl::kNumFields = 6;
882 const int MPSReaderImpl::kFieldStartPos[kNumFields] = {1, 4, 14, 24, 39, 49};
883 const int MPSReaderImpl::kFieldLength[kNumFields] = {2, 8, 8, 12, 8, 12};
884 const int MPSReaderImpl::kSpacePos[12] = {12, 13, 22, 23, 36, 37,
885 38, 47, 48, 61, 62, 63};
890 section_(UNKNOWN_SECTION),
891 section_name_to_id_map_(),
892 row_name_to_id_map_(),
893 bound_name_to_id_map_(),
894 integer_type_names_set_(),
897 in_integer_section_(false),
898 num_unconstrained_rows_(0) {
899 section_name_to_id_map_[
"*"] = COMMENT;
900 section_name_to_id_map_[
"NAME"] = NAME;
901 section_name_to_id_map_[
"OBJSENSE"] = OBJSENSE;
902 section_name_to_id_map_[
"ROWS"] = ROWS;
903 section_name_to_id_map_[
"LAZYCONS"] = LAZYCONS;
904 section_name_to_id_map_[
"COLUMNS"] = COLUMNS;
905 section_name_to_id_map_[
"RHS"] = RHS;
906 section_name_to_id_map_[
"RANGES"] = RANGES;
907 section_name_to_id_map_[
"BOUNDS"] = BOUNDS;
908 section_name_to_id_map_[
"INDICATORS"] = INDICATORS;
909 section_name_to_id_map_[
"ENDATA"] = ENDATA;
910 row_name_to_id_map_[
"E"] = EQUALITY;
911 row_name_to_id_map_[
"L"] = LESS_THAN;
912 row_name_to_id_map_[
"G"] = GREATER_THAN;
913 row_name_to_id_map_[
"N"] = NONE;
914 bound_name_to_id_map_[
"LO"] = LOWER_BOUND;
915 bound_name_to_id_map_[
"UP"] = UPPER_BOUND;
916 bound_name_to_id_map_[
"FX"] = FIXED_VARIABLE;
917 bound_name_to_id_map_[
"FR"] = FREE_VARIABLE;
918 bound_name_to_id_map_[
"MI"] = INFINITE_LOWER_BOUND;
919 bound_name_to_id_map_[
"PL"] = INFINITE_UPPER_BOUND;
920 bound_name_to_id_map_[
"BV"] = BINARY;
921 bound_name_to_id_map_[
"LI"] = LOWER_BOUND;
922 bound_name_to_id_map_[
"UI"] = UPPER_BOUND;
923 integer_type_names_set_.insert(
"BV");
924 integer_type_names_set_.insert(
"LI");
925 integer_type_names_set_.insert(
"UI");
928 void MPSReaderImpl::Reset() {
929 fields_.resize(kNumFields);
931 in_integer_section_ =
false;
932 num_unconstrained_rows_ = 0;
933 objective_name_.clear();
936 void MPSReaderImpl::DisplaySummary() {
937 if (num_unconstrained_rows_ > 0) {
938 VLOG(1) <<
"There are " << num_unconstrained_rows_ + 1
939 <<
" unconstrained rows. The first of them (" << objective_name_
940 <<
") was used as the objective.";
944 bool MPSReaderImpl::IsFixedFormat() {
945 for (
const int i : kSpacePos) {
946 if (i >= line_.length())
break;
947 if (line_[i] !=
' ')
return false;
952 absl::Status MPSReaderImpl::SplitLineIntoFields() {
954 fields_ = absl::StrSplit(line_, absl::ByAnyChar(
" \t"), absl::SkipEmpty());
955 if (fields_.size() > kNumFields) {
956 return InvalidArgumentError(
"Found too many fields.");
963 if (section_ != NAME && !IsFixedFormat()) {
964 return InvalidArgumentError(
"Line is not in fixed format.");
966 const int length = line_.length();
967 for (
int i = 0; i < kNumFields; ++i) {
968 if (kFieldStartPos[i] < length) {
969 fields_[i] = line_.substr(kFieldStartPos[i], kFieldLength[i]);
970 fields_[i].erase(fields_[i].find_last_not_of(
" ") + 1);
976 return absl::OkStatus();
979 std::string MPSReaderImpl::GetFirstWord()
const {
980 if (line_[0] ==
' ') {
981 return std::string(
"");
983 const int first_space_pos = line_.find(
' ');
984 const std::string first_word = line_.substr(0, first_space_pos);
988 bool MPSReaderImpl::IsCommentOrBlank()
const {
989 const char* line = line_.c_str();
993 for (; *line !=
'\0'; ++line) {
994 if (*line !=
' ' && *line !=
'\t') {
1001 absl::StatusOr<double> MPSReaderImpl::GetDoubleFromString(
1002 const std::string& str) {
1004 if (!absl::SimpleAtod(str, &result)) {
1005 return InvalidArgumentError(
1006 absl::StrCat(
"Failed to convert \"", str,
"\" to double."));
1008 if (std::isnan(result)) {
1009 return InvalidArgumentError(
"Found NaN value.");
1014 absl::StatusOr<bool> MPSReaderImpl::GetBoolFromString(
const std::string& str) {
1016 if (!absl::SimpleAtoi(str, &result) || result < 0 || result > 1) {
1017 return InvalidArgumentError(
1018 absl::StrCat(
"Failed to convert \"", str,
"\" to bool."));
1023 absl::Status MPSReaderImpl::ProcessSosSection() {
1024 return InvalidArgumentError(
"Section SOS currently not supported.");
1027 absl::Status MPSReaderImpl::InvalidArgumentError(
1028 const std::string& error_message) {
1029 return AppendLineToError(absl::InvalidArgumentError(error_message));
1032 absl::Status MPSReaderImpl::AppendLineToError(
const absl::Status& status) {
1034 <<
" Line " << line_num_ <<
": \"" << line_ <<
"\".";
::operations_research::MPIndicatorConstraint * mutable_indicator_constraint()
void set_lower_bound(double value)
::operations_research::MPConstraintProto * mutable_constraint()
void SetObjectiveCoefficient(int index, double coefficient)
StatusBuilder & SetAppend()
bool VariableIsInteger(int index)
void SetConstraintBounds(int index, double lower_bound, double upper_bound)
bool VariableIsInteger(int index)
void SetConstraintCoefficient(int row_index, int col_index, double coefficient)
DataWrapper(LinearProgram *data)
void SetIsLazy(int row_index)
#define VLOG(verboselevel)
void SetConstraintBounds(int index, double lower_bound, double upper_bound)
void set_name(ArgT0 &&arg0, ArgT... args)
void set_var_index(::PROTOBUF_NAMESPACE_ID::int32 value)
void SetVariableBounds(int index, double lower_bound, double upper_bound)
double VariableLowerBound(int index)
void SetConstraintCoefficient(int row_index, int col_index, double coefficient)
double ConstraintUpperBound(int row_index)
void SetObjectiveDirection(bool maximize)
void SetObjectiveDirection(bool maximize)
void add_coefficient(double value)
void SetVariableBounds(int index, double lower_bound, double upper_bound)
double ConstraintUpperBound(int row_index)
void set_var_value(::PROTOBUF_NAMESPACE_ID::int32 value)
double VariableLowerBound(int index)
int FindOrCreateVariable(const std::string &name)
double ConstraintLowerBound(int row_index)
absl::Status ParseFile(const std::string &file_name, Data *data, MPSReader::Form form)
void SetObjectiveCoefficient(int index, double coefficient)
DataWrapper(MPModelProto *data)
int FindOrCreateConstraint(const std::string &name)
double VariableUpperBound(int index)
void SetVariableTypeToInteger(int index)
absl::Status CreateIndicatorConstraint(std::string row_name, int col_index, bool col_value)
void set_name(ArgT0 &&arg0, ArgT... args)
#define DCHECK(condition)
void set_lower_bound(double value)
const Collection::value_type::second_type & FindWithDefault(const Collection &collection, const typename Collection::value_type::first_type &key, const typename Collection::value_type::second_type &value)
void set_upper_bound(double value)
void SetIsLazy(int row_index)
#define LOG_FIRST_N(severity, n)
absl::Status ParseFile(const std::string &file_name, LinearProgram *data, Form form=AUTO_DETECT)
void set_name(ArgT0 &&arg0, ArgT... args)
Collection of objects used to extend the Constraint Solver library.
double VariableUpperBound(int index)
#define RETURN_IF_ERROR(expr)
double ConstraintLowerBound(int row_index)
void SetVariableTypeToInteger(int index)
void SetName(const std::string &name)
int FindOrCreateVariable(const std::string &name)
int FindOrCreateConstraint(const std::string &name)
void add_var_index(::PROTOBUF_NAMESPACE_ID::int32 value)
#define ASSIGN_OR_RETURN(lhs, rexpr)
int RemoveAt(RepeatedType *array, const IndexContainer &indices)
void SetName(const std::string &name)
absl::Status CreateIndicatorConstraint(std::string cst_name, int var_index, bool var_value)