3333
3434#include < algorithm>
3535#include < array>
36- #include < cpptoml.h>
3736#include < cmath>
37+ #include < cpptoml.h>
3838#include < cstdint>
3939#include < cstring>
4040#include < fstream>
@@ -64,15 +64,13 @@ constexpr uint32_t ClassicalModelDrawHeadVersion = 14;
6464constexpr uint32_t ModelPolicyTableCount = 1 ;
6565constexpr uint32_t ModelPolicyContextCount = Evaluation::PolicyStoredContextCount;
6666constexpr uint32_t ModelBlendComponentCount = 0 ;
67- constexpr uint32_t ModelDrawHeadParameterCount =
68- Evaluation::ClassicalDrawHead::ParameterCount;
67+ constexpr uint32_t ModelDrawHeadParameterCount = Evaluation::ClassicalDrawHead::ParameterCount;
6968constexpr uint32_t PolicyCrossPatternCount = Evaluation::PolicyStoredPatternCount;
7069constexpr uint32_t PolicyCrossPayloadBytes =
7170 ModelPolicyContextCount * PolicyCrossPatternCount * PolicyCrossPatternCount * sizeof (Score);
7271constexpr uint32_t ClassicalModelCompactP3PayloadBytes = PolicyCrossPayloadBytes;
7372constexpr uint32_t ClassicalModelDrawHeadPayloadBytes =
74- PolicyCrossPayloadBytes + sizeof (uint32_t )
75- + ModelDrawHeadParameterCount * sizeof (double );
73+ PolicyCrossPayloadBytes + sizeof (uint32_t ) + ModelDrawHeadParameterCount * sizeof (double );
7674constexpr double ClassicalDrawHeadCoefficientLimit = 64.0 ;
7775
7876static_assert (sizeof (double ) == 8 && std::numeric_limits<double >::is_iec559);
@@ -94,14 +92,16 @@ float ScalingFactor = 200.0f;
9492// Classical evaluation and score tables
9593// Note that Renju has asymmetry eval and score
9694
97- Eval EVALS [RULE_NB + 1 ][PCODE_NB ];
98- Eval EVALS_THREAT [RULE_NB + 1 ][THREAT_NB ];
99- MoveScorePair P4SCORES [RULE_NB + 1 ][PCODE_NB ];
100- ClassicalDrawHead CLASSICAL_DRAW_HEAD ;
95+ Eval EVALS [RULE_NB + 1 ][PCODE_NB ];
96+ Eval EVALS_THREAT [RULE_NB + 1 ][THREAT_NB ];
97+ MoveScorePair P4SCORES [RULE_NB + 1 ][PCODE_NB ];
98+ ClassicalDrawHead CLASSICAL_DRAW_HEAD ;
10199std::array<float , VALUE_EVAL_MAX - VALUE_EVAL_MIN + 1 > CLASSICAL_DECISIVE_WIN_RATE {};
102- Score POLICY_CROSS [RULE_NB + 1 ][PolicyContextCount][PATTERN4_NB ][PATTERN4_NB ];
103- uint8_t POLICY_CROSS_ACTIVE_MASK [RULE_NB + 1 ] = {};
104- bool POLICY_CROSS_PRESENT = false ;
100+ ClassicalValueReadout CLASSICAL_VALUE_READOUT ;
101+ std::array<Value, VALUE_EVAL_MAX - VALUE_EVAL_MIN + 1 > CLASSICAL_VALUE_READOUT_CACHE {};
102+ Score POLICY_CROSS [RULE_NB + 1 ][PolicyContextCount][PATTERN4_NB ][PATTERN4_NB ];
103+ uint8_t POLICY_CROSS_ACTIVE_MASK [RULE_NB + 1 ] = {};
104+ bool POLICY_CROSS_PRESENT = false ;
105105
106106void resetClassicalDrawHead ()
107107{
@@ -130,6 +130,38 @@ float classicalDecisiveWinRate(Value rawValue)
130130 return CLASSICAL_DECISIVE_WIN_RATE [int (rawValue) - VALUE_EVAL_MIN ];
131131}
132132
133+ void refreshClassicalValueReadoutCache ()
134+ {
135+ static constexpr std::array<double , ClassicalValueReadout::KnotCount> XKnots =
136+ {0.0 , 0.5 , 1.0 , 2.0 , 4.0 , 8.0 };
137+ const auto &yKnots = CLASSICAL_VALUE_READOUT .knots ;
138+ const double scale = ScalingFactor;
139+
140+ for (int raw = VALUE_EVAL_MIN ; raw <= VALUE_EVAL_MAX ; raw++) {
141+ double x = std::abs (double (raw)) / scale;
142+ double y;
143+ if (x >= XKnots.back ())
144+ y = yKnots.back () + x - XKnots.back ();
145+ else {
146+ size_t hi = 1 ;
147+ while (x > XKnots[hi])
148+ hi++;
149+ size_t lo = hi - 1 ;
150+ double t = (x - XKnots[lo]) / (XKnots[hi] - XKnots[lo]);
151+ y = yKnots[lo] + t * (yKnots[hi] - yKnots[lo]);
152+ }
153+ double mapped = std::copysign (y * scale, double (raw));
154+ CLASSICAL_VALUE_READOUT_CACHE [raw - VALUE_EVAL_MIN ] =
155+ Value (std::clamp<long long >(std::llround (mapped), VALUE_EVAL_MIN , VALUE_EVAL_MAX ));
156+ }
157+ }
158+
159+ Value mapClassicalValue (Value rawValue)
160+ {
161+ assert (VALUE_EVAL_MIN <= rawValue && rawValue <= VALUE_EVAL_MAX );
162+ return CLASSICAL_VALUE_READOUT_CACHE [int (rawValue) - VALUE_EVAL_MIN ];
163+ }
164+
133165void resetPolicyCross ()
134166{
135167 std::memset (POLICY_CROSS , 0 , sizeof (POLICY_CROSS ));
@@ -183,11 +215,12 @@ GeneralConfig GeneralCfg;
183215// / Nothing is published until loadConfig's commit step.
184216struct PendingConfig
185217{
186- GeneralConfig general = GeneralCfg;
187- Search::SearchConfig search = Search::SearchCfg;
188- Search::TimeConfig time = Search::TimeCfg;
189- Database::DatabaseConfig database = Database::DatabaseCfg;
190- Evaluation::EvaluatorConfig eval = Evaluation::EvalCfg;
218+ GeneralConfig general = GeneralCfg;
219+ Search::SearchConfig search = Search::SearchCfg;
220+ Search::TimeConfig time = Search::TimeCfg;
221+ Database::DatabaseConfig database = Database::DatabaseCfg;
222+ Evaluation::EvaluatorConfig eval = Evaluation::EvalCfg;
223+ Evaluation::ClassicalValueReadout valueReadout = Evaluation::CLASSICAL_VALUE_READOUT ;
191224
192225 // / "[search] default_searcher" was present: switch the searcher at commit.
193226 std::optional<std::string> searcherName;
@@ -276,11 +309,14 @@ bool Config::loadConfig(std::istream &configStream)
276309 }
277310
278311 // Commit: publish the parsed structs, then apply the engine effects.
279- GeneralCfg = pending.general ;
280- Search::SearchCfg = pending.search ;
281- Search::TimeCfg = pending.time ;
282- Database::DatabaseCfg = pending.database ;
283- Evaluation::EvalCfg = pending.eval ;
312+ GeneralCfg = pending.general ;
313+ Search::SearchCfg = pending.search ;
314+ Search::TimeCfg = pending.time ;
315+ Database::DatabaseCfg = pending.database ;
316+ Evaluation::EvalCfg = pending.eval ;
317+ Evaluation::CLASSICAL_VALUE_READOUT = pending.valueReadout ;
318+ if (Evaluation::CLASSICAL_VALUE_READOUT .knotsActive )
319+ Evaluation::refreshClassicalValueReadoutCache ();
284320
285321 // The searcher switch precedes the TT resize: setupSearcher carries the
286322 // old searcher's memory limit onto the new one, and the resize then
@@ -309,7 +345,7 @@ void Config::readRequirement(const cpptoml::table &t)
309345{
310346 auto [major, minor, revision] = getVersionNumbers ();
311347 uint64_t rapVer = ((uint64_t )major << 32 ) | ((uint64_t )minor << 16 ) | (uint64_t )revision;
312- auto composeVersion = [](const std::vector<int64_t > &ver, const char *key) {
348+ auto composeVersion = [](const std::vector<int64_t > &ver, const char *key) {
313349 if (ver.size () != 3 )
314350 throw std::runtime_error (std::string (" illegal " ) + key);
315351 for (int64_t component : ver)
@@ -509,6 +545,12 @@ void Config::readModel(const cpptoml::table &t, PendingConfig &pending)
509545 const Rule Rules[] = {FREESTYLE , STANDARD , RENJU };
510546 const char *RuleName[] = {" freestyle" , " standard" , " renju" };
511547
548+ uint64_t nextRevision = Evaluation::CLASSICAL_VALUE_READOUT .revision + 1 ;
549+ if (nextRevision == 0 )
550+ nextRevision = 1 ;
551+ pending.valueReadout = {};
552+ pending.valueReadout .revision = nextRevision;
553+
512554 std::string modelPath = t.get_as <std::string>(" binary_file" ).value_or (" " );
513555 if (!modelPath.empty ()) {
514556 if (!Command::loadModelFromFile (modelPath))
@@ -607,10 +649,50 @@ void Config::readModel(const cpptoml::table &t, PendingConfig &pending)
607649 double configuredScalingFactor =
608650 t.get_as <double >(" scaling_factor" ).value_or (Evaluation::ScalingFactor);
609651 float runtimeScalingFactor = static_cast <float >(configuredScalingFactor);
610- if (Evaluation::isClassicalDrawHeadActive ()
652+ auto readoutKnots = t.get_array_of <double >(" value_readout_knots" );
653+ if (readoutKnots) {
654+ if (readoutKnots->size () != Evaluation::ClassicalValueReadout::KnotCount)
655+ throw std::runtime_error (" value_readout_knots must contain 6 values" );
656+ for (size_t i = 0 ; i < readoutKnots->size (); i++) {
657+ double value = (*readoutKnots)[i];
658+ if (!std::isfinite (value) || value < 0.0 || value > 64.0 || (i == 0 && value != 0.0 )
659+ || (i != 0 && value < (*readoutKnots)[i - 1 ]))
660+ throw std::runtime_error (" value_readout_knots must be finite, monotone, start at "
661+ " zero, and not exceed 64" );
662+ pending.valueReadout .knots [i] = value;
663+ }
664+ pending.valueReadout .knotsActive = true ;
665+ }
666+
667+ auto readoutContext = t.get_array_of <double >(" value_readout_context" );
668+ if (readoutContext) {
669+ if (!readoutKnots)
670+ throw std::runtime_error (" value_readout_context requires value_readout_knots" );
671+ if (readoutContext->size () != Evaluation::ClassicalValueReadout::ContextCount)
672+ throw std::runtime_error (" value_readout_context must contain 4 values" );
673+ for (size_t i = 0 ; i < readoutContext->size (); i++) {
674+ double value = (*readoutContext)[i];
675+ if (!std::isfinite (value) || std::abs (value) > 8.0 )
676+ throw std::runtime_error (
677+ " value_readout_context values must be finite and within [-8, 8]" );
678+ pending.valueReadout .context [i] = value;
679+ }
680+ pending.valueReadout .contextActive =
681+ std::any_of (pending.valueReadout .context .begin (),
682+ pending.valueReadout .context .end (),
683+ [](double coefficient) { return coefficient != 0.0 ; });
684+ }
685+
686+ double concentration = t.get_as <double >(" value_concentration" ).value_or (0.0 );
687+ if (!std::isfinite (concentration) || std::abs (concentration) > 64.0 )
688+ throw std::runtime_error (" value_concentration must be finite and within [-64, 64]" );
689+ pending.valueReadout .concentration = concentration;
690+
691+ if ((Evaluation::isClassicalDrawHeadActive () || pending.valueReadout .knotsActive
692+ || pending.valueReadout .concentration != 0.0 )
611693 && (!std::isfinite (runtimeScalingFactor) || runtimeScalingFactor <= 0 .0f ))
612694 throw std::runtime_error (
613- " classical draw head requires a finite positive scaling_factor" );
695+ " classical value post-processing requires a finite positive scaling_factor" );
614696 Evaluation::ScalingFactor = runtimeScalingFactor;
615697 if (Evaluation::isClassicalDrawHeadActive ())
616698 Evaluation::refreshClassicalDrawHeadCache ();
@@ -950,8 +1032,7 @@ bool Config::loadModel(std::istream &inStream)
9501032 in->read (reinterpret_cast <char *>(&componentCount), sizeof (componentCount));
9511033 in->read (reinterpret_cast <char *>(&patternCount), sizeof (patternCount));
9521034 if (!*in || magic != ClassicalModelExtensionMagic
953- || (version != ClassicalModelCompactP3Version
954- && version != ClassicalModelDrawHeadVersion)
1035+ || (version != ClassicalModelCompactP3Version && version != ClassicalModelDrawHeadVersion)
9551036 || tableCount != ModelPolicyTableCount || contextCount != ModelPolicyContextCount
9561037 || patternCount != PolicyCrossPatternCount)
9571038 return false ;
@@ -1040,11 +1121,11 @@ void Config::exportModel(std::ostream &outStream)
10401121 writeDrawHead ? ClassicalModelDrawHeadVersion : ClassicalModelCompactP3Version;
10411122 uint32_t payloadBytes =
10421123 writeDrawHead ? ClassicalModelDrawHeadPayloadBytes : ClassicalModelCompactP3PayloadBytes;
1043- uint32_t tableCount = ModelPolicyTableCount;
1044- uint32_t contextCount = ModelPolicyContextCount;
1124+ uint32_t tableCount = ModelPolicyTableCount;
1125+ uint32_t contextCount = ModelPolicyContextCount;
10451126 uint32_t componentCount =
10461127 writeDrawHead ? ModelDrawHeadParameterCount : ModelBlendComponentCount;
1047- uint32_t patternCount = PolicyCrossPatternCount;
1128+ uint32_t patternCount = PolicyCrossPatternCount;
10481129 out->write (reinterpret_cast <const char *>(&version), sizeof (version));
10491130 out->write (reinterpret_cast <const char *>(&payloadBytes), sizeof (payloadBytes));
10501131 out->write (reinterpret_cast <const char *>(&tableCount), sizeof (tableCount));
0 commit comments