diff --git a/.github/workflows/matetrack.yml b/.github/workflows/matetrack.yml index de65209fb..dc8dff8d5 100644 --- a/.github/workflows/matetrack.yml +++ b/.github/workflows/matetrack.yml @@ -24,15 +24,31 @@ jobs: with: repository: vondele/matetrack path: matetrack - ref: 20287a1a145f30a166b7ef251eddb611e4e44fbf + ref: 814160f82e6428ed2f6522dc06c2a6fa539cd413 persist-credentials: false - name: matetrack install deps working-directory: matetrack run: pip install -r requirements.txt + - name: cache syzygy + id: cache-syzygy + uses: actions/cache@v4 + with: + path: | + matetrack/3-4-5-wdl/ + matetrack/3-4-5-dtz/ + key: key-syzygy + + - name: download syzygy 3-4-5 if needed + working-directory: matetrack + if: steps.cache-syzygy.outputs.cache-hit != 'true' + run: | + wget --no-verbose -r -nH --cut-dirs=2 --no-parent --reject="index.html*" -e robots=off https://tablebase.lichess.ovh/tables/standard/3-4-5-wdl/ + wget --no-verbose -r -nH --cut-dirs=2 --no-parent --reject="index.html*" -e robots=off https://tablebase.lichess.ovh/tables/standard/3-4-5-dtz/ + - name: Run matetrack working-directory: matetrack run: | - python matecheck.py --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 | tee matecheckout.out + python matecheck.py --syzygyPath 3-4-5-wdl/:3-4-5-dtz/ --engine /home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish --epdFile mates2000.epd --nodes 100000 | tee matecheckout.out ! grep "issues were detected" matecheckout.out > /dev/null diff --git a/AUTHORS b/AUTHORS index 6957682f4..1ac40d879 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,6 +20,7 @@ Alexander Kure Alexander Pagel (Lolligerhans) Alfredo Menezes (lonfom169) Ali AlZhrani (Cooffe) +Andreas Jan van der Meulen (Andyson007) Andreas Matthies (Matthies) Andrei Vetrov (proukornew) Andrew Grant (AndyGrant) diff --git a/src/engine.cpp b/src/engine.cpp index a2e3ca44f..54ba25adf 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -93,7 +93,7 @@ Engine::Engine(std::string path) : options["UCI_LimitStrength"] << Option(false); options["UCI_Elo"] << Option(1320, 1320, 3190); options["UCI_ShowWDL"] << Option(false); - options["SyzygyPath"] << Option("", [](const Option& o) { + options["SyzygyPath"] << Option("", [](const Option& o) { Tablebases::init(o); return std::nullopt; }); diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 44890a361..d0c553ffc 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -66,24 +66,23 @@ Value Eval::evaluate(const Eval::NNUE::Networks& networks, auto [psqt, positional] = smallNet ? networks.small.evaluate(pos, &caches.small) : networks.big.evaluate(pos, &caches.big); - Value nnue = (125 * psqt + 131 * positional) / 128; - int nnueComplexity = std::abs(psqt - positional); + Value nnue = (125 * psqt + 131 * positional) / 128; // Re-evaluate the position when higher eval accuracy is worth the time spent if (smallNet && (nnue * simpleEval < 0 || std::abs(nnue) < 227)) { std::tie(psqt, positional) = networks.big.evaluate(pos, &caches.big); nnue = (125 * psqt + 131 * positional) / 128; - nnueComplexity = std::abs(psqt - positional); smallNet = false; } // Blend optimism and eval with nnue complexity - optimism += optimism * nnueComplexity / 457; - nnue -= nnue * nnueComplexity / 19157; + int nnueComplexity = std::abs(psqt - positional); + optimism += optimism * nnueComplexity / (smallNet ? 433 : 453); + nnue -= nnue * nnueComplexity / (smallNet ? 18815 : 17864); - int material = 554 * pos.count() + pos.non_pawn_material(); - v = (nnue * (73921 + material) + optimism * (8112 + material)) / 73260; + int material = (smallNet ? 553 : 532) * pos.count() + pos.non_pawn_material(); + v = (nnue * (73921 + material) + optimism * (8112 + material)) / (smallNet ? 68104 : 74715); // Evaluation grain (to get more alpha-beta cuts) with randomization (for robustness) v = (v / 16) * 16 - 1 + (pos.key() & 0x2); diff --git a/src/movepick.cpp b/src/movepick.cpp index c21b14a90..55bacf6e7 100644 --- a/src/movepick.cpp +++ b/src/movepick.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include "bitboard.h" @@ -35,7 +34,6 @@ enum Stages { MAIN_TT, CAPTURE_INIT, GOOD_CAPTURE, - REFUTATION, QUIET_INIT, GOOD_QUIET, BAD_CAPTURE, @@ -91,38 +89,21 @@ MovePicker::MovePicker(const Position& p, const CapturePieceToHistory* cph, const PieceToHistory** ch, const PawnHistory* ph, - const Move* killers) : + Move km) : pos(p), mainHistory(mh), captureHistory(cph), continuationHistory(ch), pawnHistory(ph), ttMove(ttm), - refutations{{killers[0], 0}, {killers[1], 0}}, + killer(km), depth(d) { - assert(d > 0); - stage = (pos.checkers() ? EVASION_TT : MAIN_TT) + !(ttm && pos.pseudo_legal(ttm)); -} + if (pos.checkers()) + stage = EVASION_TT + !(ttm && pos.pseudo_legal(ttm)); -// Constructor for quiescence search -MovePicker::MovePicker(const Position& p, - Move ttm, - Depth d, - const ButterflyHistory* mh, - const CapturePieceToHistory* cph, - const PieceToHistory** ch, - const PawnHistory* ph) : - pos(p), - mainHistory(mh), - captureHistory(cph), - continuationHistory(ch), - pawnHistory(ph), - ttMove(ttm), - depth(d) { - assert(d <= 0); - - stage = (pos.checkers() ? EVASION_TT : QSEARCH_TT) + !(ttm && pos.pseudo_legal(ttm)); + else + stage = (depth > 0 ? MAIN_TT : QSEARCH_TT) + !(ttm && pos.pseudo_legal(ttm)); } // Constructor for ProbCut: we generate captures with SEE greater than or equal @@ -185,6 +166,8 @@ void MovePicker::score() { m.value += (*continuationHistory[3])[pc][to]; m.value += (*continuationHistory[5])[pc][to]; + m.value += (m == killer) * 65536; + // bonus for checks m.value += bool(pos.check_squares(pt) & to) * 16384; @@ -268,18 +251,6 @@ top: })) return *(cur - 1); - // Prepare the pointers to loop over the refutations array - cur = std::begin(refutations); - endMoves = std::end(refutations); - - ++stage; - [[fallthrough]]; - - case REFUTATION : - if (select([&]() { - return *cur != Move::none() && !pos.capture_stage(*cur) && pos.pseudo_legal(*cur); - })) - return *(cur - 1); ++stage; [[fallthrough]]; @@ -297,8 +268,7 @@ top: [[fallthrough]]; case GOOD_QUIET : - if (!skipQuiets - && select([&]() { return *cur != refutations[0] && *cur != refutations[1]; })) + if (!skipQuiets && select([]() { return true; })) { if ((cur - 1)->value > -7998 || (cur - 1)->value <= quiet_threshold(depth)) return *(cur - 1); @@ -327,7 +297,7 @@ top: case BAD_QUIET : if (!skipQuiets) - return select([&]() { return *cur != refutations[0] && *cur != refutations[1]; }); + return select([]() { return true; }); return Move::none(); diff --git a/src/movepick.h b/src/movepick.h index 2564f7301..92e11de2b 100644 --- a/src/movepick.h +++ b/src/movepick.h @@ -160,14 +160,7 @@ class MovePicker { const CapturePieceToHistory*, const PieceToHistory**, const PawnHistory*, - const Move*); - MovePicker(const Position&, - Move, - Depth, - const ButterflyHistory*, - const CapturePieceToHistory*, - const PieceToHistory**, - const PawnHistory*); + Move killer = Move::none()); MovePicker(const Position&, Move, int, const CapturePieceToHistory*); Move next_move(bool skipQuiets = false); @@ -184,12 +177,12 @@ class MovePicker { const CapturePieceToHistory* captureHistory; const PieceToHistory** continuationHistory; const PawnHistory* pawnHistory; - Move ttMove; - ExtMove refutations[2], *cur, *endMoves, *endBadCaptures, *beginBadQuiets, *endBadQuiets; - int stage; - int threshold; - Depth depth; - ExtMove moves[MAX_MOVES]; + Move ttMove, killer; + ExtMove * cur, *endMoves, *endBadCaptures, *beginBadQuiets, *endBadQuiets; + int stage; + int threshold; + Depth depth; + ExtMove moves[MAX_MOVES]; }; } // namespace Stockfish diff --git a/src/search.cpp b/src/search.cpp index 64313f833..13b4e9ae2 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -76,14 +76,14 @@ Value futility_margin(Depth d, bool noTtCutNode, bool improving, bool oppWorseni } constexpr int futility_move_count(bool improving, Depth depth) { - return improving ? (3 + depth * depth) : (3 + depth * depth) / 2; + return (3 + depth * depth) / (2 - improving); } // Add correctionHistory value to raw staticEval and guarantee evaluation // does not hit the tablebase range. Value to_corrected_static_eval(Value v, const Worker& w, const Position& pos) { auto cv = w.correctionHistory[pos.side_to_move()][pawn_structure_index(pos)]; - v += cv * std::abs(cv) / 5073; + v += 66 * cv / 512; return std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); } @@ -91,7 +91,7 @@ Value to_corrected_static_eval(Value v, const Worker& w, const Position& pos) { int stat_bonus(Depth d) { return std::min(190 * d - 108, 1596); } // History and stats update malus, based on depth -int stat_malus(Depth d) { return (d < 4 ? 736 * d - 268 : 2044); } +int stat_malus(Depth d) { return std::min(736 * d - 268, 2044); } // Add a small random component to draw evaluations to avoid 3-fold blindness Value value_draw(size_t nodes) { return VALUE_DRAW - 1 + Value(nodes & 0x2); } @@ -124,21 +124,19 @@ Value value_to_tt(Value v, int ply); Value value_from_tt(Value v, int ply, int r50c); void update_pv(Move* pv, Move move, const Move* childPv); void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus); -void update_refutations(Stack* ss, Move move); +void update_killer(Stack* ss, Move move); void update_quiet_histories( const Position& pos, Stack* ss, Search::Worker& workerThread, Move move, int bonus); void update_quiet_stats( const Position& pos, Stack* ss, Search::Worker& workerThread, Move move, int bonus); -void update_all_stats(const Position& pos, - Stack* ss, - Search::Worker& workerThread, - Move bestMove, - Square prevSq, - Move* quietsSearched, - int quietCount, - Move* capturesSearched, - int captureCount, - Depth depth); +void update_all_stats(const Position& pos, + Stack* ss, + Search::Worker& workerThread, + Move bestMove, + Square prevSq, + ValueList& quietsSearched, + ValueList& capturesSearched, + Depth depth); } // namespace @@ -362,7 +360,7 @@ void Search::Worker::iterative_deepening() { // Reset aspiration window starting size Value avg = rootMoves[pvIdx].averageScore; - delta = 9 + avg * avg / 10424; + delta = 5 + avg * avg / 13424; alpha = std::max(avg - delta, -VALUE_INFINITE); beta = std::min(avg + delta, VALUE_INFINITE); @@ -602,7 +600,7 @@ Value Search::Worker::search( assert(0 < depth && depth < MAX_PLY); assert(!(PvNode && cutNode)); - Move pv[MAX_PLY + 1], capturesSearched[32], quietsSearched[32]; + Move pv[MAX_PLY + 1]; StateInfo st; ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize); @@ -611,18 +609,20 @@ Value Search::Worker::search( Depth extension, newDepth; Value bestValue, value, eval, maxValue, probCutBeta; bool givesCheck, improving, priorCapture, opponentWorsening; - bool capture, moveCountPruning, ttCapture; + bool capture, ttCapture; Piece movedPiece; - int moveCount, captureCount, quietCount; + + ValueList capturesSearched; + ValueList quietsSearched; // Step 1. Initialize node Worker* thisThread = this; ss->inCheck = pos.checkers(); priorCapture = pos.captured_piece(); Color us = pos.side_to_move(); - moveCount = captureCount = quietCount = ss->moveCount = 0; - bestValue = -VALUE_INFINITE; - maxValue = VALUE_INFINITE; + ss->moveCount = 0; + bestValue = -VALUE_INFINITE; + maxValue = VALUE_INFINITE; // Check for the available remaining time if (is_mainthread()) @@ -656,9 +656,9 @@ Value Search::Worker::search( assert(0 <= ss->ply && ss->ply < MAX_PLY); - bestMove = Move::none(); - (ss + 2)->killers[0] = (ss + 2)->killers[1] = Move::none(); - (ss + 2)->cutoffCnt = 0; + bestMove = Move::none(); + (ss + 1)->killer = Move::none(); + (ss + 2)->cutoffCnt = 0; Square prevSq = ((ss - 1)->currentMove).is_ok() ? ((ss - 1)->currentMove).to_sq() : SQ_NONE; ss->statScore = 0; @@ -762,7 +762,7 @@ Value Search::Worker::search( if (ss->inCheck) { // Skip early pruning when in check - ss->staticEval = eval = VALUE_NONE; + ss->staticEval = eval = (ss - 2)->staticEval; improving = false; goto moves_loop; } @@ -814,12 +814,9 @@ Value Search::Worker::search( // Set up the improving flag, which is true if current static evaluation is // bigger than the previous static evaluation at our turn (if we were in - // check at our previous move we look at static evaluation at move prior to it - // and if we were in check at move prior to it flag is set to true) and is + // check at our previous move we go back until we weren't in check) and is // false otherwise. The improving flag is used in various pruning heuristics. - improving = (ss - 2)->staticEval != VALUE_NONE - ? ss->staticEval > (ss - 2)->staticEval - : (ss - 4)->staticEval != VALUE_NONE && ss->staticEval > (ss - 4)->staticEval; + improving = ss->staticEval > (ss - 2)->staticEval; opponentWorsening = ss->staticEval + (ss - 1)->staticEval > 2; @@ -844,7 +841,7 @@ Value Search::Worker::search( return beta + (eval - beta) / 3; // Step 9. Null move search with verification search (~35 Elo) - if (!PvNode && (ss - 1)->currentMove != Move::null() && (ss - 1)->statScore < 14389 + if (cutNode && (ss - 1)->currentMove != Move::null() && (ss - 1)->statScore < 14389 && eval >= beta && ss->staticEval >= beta - 21 * depth + 390 && !excludedMove && pos.non_pawn_material(us) && ss->ply >= thisThread->nmpMinPly && beta > VALUE_TB_LOSS_IN_MAX_PLY) @@ -859,7 +856,7 @@ Value Search::Worker::search( pos.do_null_move(st, tt); - Value nullValue = -search(pos, ss + 1, -beta, -beta + 1, depth - R, !cutNode); + Value nullValue = -search(pos, ss + 1, -beta, -beta + 1, depth - R, false); pos.undo_null_move(); @@ -985,10 +982,12 @@ moves_loop: // When in check, search starts here MovePicker mp(pos, ttData.move, depth, &thisThread->mainHistory, &thisThread->captureHistory, - contHist, &thisThread->pawnHistory, ss->killers); + contHist, &thisThread->pawnHistory, ss->killer); - value = bestValue; - moveCountPruning = false; + value = bestValue; + + int moveCount = 0; + bool moveCountPruning = false; // Step 13. Loop through all pseudo-legal moves until no moves remain // or a beta cutoff occurs. @@ -1071,7 +1070,7 @@ moves_loop: // When in check, search starts here + thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][move.to_sq()]; // Continuation history based pruning (~2 Elo) - if (lmrDepth < 6 && history < -4165 * depth) + if (history < -4165 * depth) continue; history += 2 * thisThread->mainHistory[us][move.from_to()]; @@ -1086,7 +1085,7 @@ moves_loop: // When in check, search starts here { if (bestValue <= futilityValue && std::abs(bestValue) < VALUE_TB_WIN_IN_MAX_PLY && futilityValue < VALUE_TB_WIN_IN_MAX_PLY) - bestValue = (bestValue + futilityValue * 3) / 4; + bestValue = futilityValue; continue; } @@ -1208,7 +1207,7 @@ moves_loop: // When in check, search starts here // Increase reduction for cut nodes (~4 Elo) if (cutNode) r += 2 - (ttData.depth >= depth && ss->ttPv) - + (!ss->ttPv && move != ttData.move && move != ss->killers[0]); + + (!ss->ttPv && move != ttData.move && move != ss->killer); // Increase reduction if ttMove is a capture (~3 Elo) if (ttCapture) @@ -1385,9 +1384,9 @@ moves_loop: // When in check, search starts here if (move != bestMove && moveCount <= 32) { if (capture) - capturesSearched[captureCount++] = move; + capturesSearched.push_back(move); else - quietsSearched[quietCount++] = move; + quietsSearched.push_back(move); } } @@ -1409,18 +1408,20 @@ moves_loop: // When in check, search starts here // If there is a move that produces search value greater than alpha, // we update the stats of searched moves. else if (bestMove) - update_all_stats(pos, ss, *this, bestMove, prevSq, quietsSearched, quietCount, - capturesSearched, captureCount, depth); + update_all_stats(pos, ss, *this, bestMove, prevSq, quietsSearched, capturesSearched, depth); // Bonus for prior countermove that caused the fail low else if (!priorCapture && prevSq != SQ_NONE) { - int bonus = (114 * (depth > 5) + 116 * (PvNode || cutNode) + 123 * ((ss - 1)->moveCount > 8) - + 64 * (!ss->inCheck && bestValue <= ss->staticEval - 108) - + 153 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 76)); + int bonus = (138 * (depth > 5) + 58 * (PvNode || cutNode) + 160 * ((ss - 1)->moveCount > 8) + + 84 * (!ss->inCheck && bestValue <= ss->staticEval - 108) + + 153 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 76) + + 32 * (!(ss - 1)->inCheck && bestValue > -(ss - 1)->staticEval + 76)); // Proportional to "how much damage we have to undo" - bonus += std::clamp(-(ss - 1)->statScore / 100, -50, 274); + bonus += std::clamp(-(ss - 1)->statScore / 100, -94, 300); + + bonus = std::max(bonus, 0); update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, stat_bonus(depth) * bonus / 100); @@ -1818,16 +1819,14 @@ void update_pv(Move* pv, Move move, const Move* childPv) { // Updates stats at the end of search() when a bestMove is found -void update_all_stats(const Position& pos, - Stack* ss, - Search::Worker& workerThread, - Move bestMove, - Square prevSq, - Move* quietsSearched, - int quietCount, - Move* capturesSearched, - int captureCount, - Depth depth) { +void update_all_stats(const Position& pos, + Stack* ss, + Search::Worker& workerThread, + Move bestMove, + Square prevSq, + ValueList& quietsSearched, + ValueList& capturesSearched, + Depth depth) { CapturePieceToHistory& captureHistory = workerThread.captureHistory; Piece moved_piece = pos.moved_piece(bestMove); @@ -1841,8 +1840,8 @@ void update_all_stats(const Position& pos, update_quiet_stats(pos, ss, workerThread, bestMove, quietMoveBonus); // Decrease stats for all non-best quiet moves - for (int i = 0; i < quietCount; ++i) - update_quiet_histories(pos, ss, workerThread, quietsSearched[i], -quietMoveMalus); + for (Move move : quietsSearched) + update_quiet_histories(pos, ss, workerThread, move, -quietMoveMalus); } else { @@ -1855,16 +1854,16 @@ void update_all_stats(const Position& pos, // main killer move in previous ply when it gets refuted. if (prevSq != SQ_NONE && ((ss - 1)->moveCount == 1 + (ss - 1)->ttHit - || ((ss - 1)->currentMove == (ss - 1)->killers[0])) + || ((ss - 1)->currentMove == (ss - 1)->killer)) && !pos.captured_piece()) update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, -quietMoveMalus); // Decrease stats for all non-best capture moves - for (int i = 0; i < captureCount; ++i) + for (Move move : capturesSearched) { - moved_piece = pos.moved_piece(capturesSearched[i]); - captured = type_of(pos.piece_on(capturesSearched[i].to_sq())); - captureHistory[moved_piece][capturesSearched[i].to_sq()][captured] << -quietMoveMalus; + moved_piece = pos.moved_piece(move); + captured = type_of(pos.piece_on(move.to_sq())); + captureHistory[moved_piece][move.to_sq()][captured] << -quietMoveMalus; } } @@ -1886,14 +1885,10 @@ void update_continuation_histories(Stack* ss, Piece pc, Square to, int bonus) { } // Updates move sorting heuristics -void update_refutations(Stack* ss, Move move) { +void update_killer(Stack* ss, Move move) { // Update killers - if (ss->killers[0] != move) - { - ss->killers[1] = ss->killers[0]; - ss->killers[0] = move; - } + ss->killer = move; } void update_quiet_histories( @@ -1912,7 +1907,7 @@ void update_quiet_histories( void update_quiet_stats( const Position& pos, Stack* ss, Search::Worker& workerThread, Move move, int bonus) { - update_refutations(ss, move); + update_killer(ss, move); update_quiet_histories(pos, ss, workerThread, move, bonus); } @@ -2012,8 +2007,12 @@ void syzygy_extend_pv(const OptionsMap& options, std::list sts; + // Step 0, do the rootMove, no correction allowed, as needed for MultiPV in TB. + auto& stRoot = sts.emplace_back(); + pos.do_move(rootMove.pv[0], stRoot); + int ply = 1; + // Step 1, walk the PV to the last position in TB with correct decisive score - int ply = 0; while (size_t(ply) < rootMove.pv.size()) { Move& pvMove = rootMove.pv[ply]; diff --git a/src/search.h b/src/search.h index adcae674d..0d983c998 100644 --- a/src/search.h +++ b/src/search.h @@ -67,7 +67,7 @@ struct Stack { int ply; Move currentMove; Move excludedMove; - Move killers[2]; + Move killer; Value staticEval; int statScore; int moveCount; diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index fb5ed4428..806211578 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -1346,7 +1346,7 @@ void Tablebases::init(const std::string& paths) { MaxCardinality = 0; TBFile::Paths = paths; - if (paths.empty() || paths == "") + if (paths.empty()) return; // MapB1H1H7[] encodes a square below a1-h8 diagonal to 0..27 diff --git a/src/ucioption.cpp b/src/ucioption.cpp index 6f5736ef2..0427c90e1 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -167,7 +167,9 @@ Option& Option::operator=(const std::string& v) { return *this; } - if (type != "button") + if (type == "string") + currentValue = v == "" ? "" : v; + else if (type != "button") currentValue = v; if (on_change) @@ -189,10 +191,16 @@ std::ostream& operator<<(std::ostream& os, const OptionsMap& om) { const Option& o = it.second; os << "\noption name " << it.first << " type " << o.type; - if (o.type == "string" || o.type == "check" || o.type == "combo") + if (o.type == "check" || o.type == "combo") os << " default " << o.defaultValue; - if (o.type == "spin") + else if (o.type == "string") + { + std::string defaultValue = o.defaultValue.empty() ? "" : o.defaultValue; + os << " default " << defaultValue; + } + + else if (o.type == "spin") os << " default " << int(stof(o.defaultValue)) << " min " << o.min << " max " << o.max;