Skip to content

Commit d8e242c

Browse files
committed
Use correct walk snapshots for UOM observation
UOM needs both the previous walk result and the current walk result after node override (that is, after user manually chooses a candidate) to make an observation, but since the nodes in a WalkResult are shared pointers to the Node instances in the grid, WalkResult does not capture an immutable snapshot. A new member function is introduced to enable getting a snapshot that has a real copy of the nodes, so as to capture the state of the grid before it is further mutated.
1 parent 5df30ff commit d8e242c

3 files changed

Lines changed: 182 additions & 1 deletion

File tree

src/Engine/UserOverrideModelTest.cpp

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <string>
2525

2626
#include "UserOverrideModel.h"
27+
#include "gramambular2/reading_grid.h"
2728
#include "gtest/gtest.h"
2829

2930
namespace McBopomofo {
@@ -119,4 +120,159 @@ TEST(UserOverrideModelTest, LRUBehavior) {
119120
ASSERT_TRUE(v.empty());
120121
}
121122

123+
constexpr char kSampleData[] = R"(
124+
ㄐㄧ 機 -3.02367199
125+
ㄐㄧ 積 -3.72854036
126+
ㄐㄧ-ㄧㄡˊ 機油 -6.03662914
127+
ㄧㄡˊ 由 -3.00970678
128+
ㄧㄡˊ 油 -3.75900671
129+
)";
130+
131+
class SimpleLM : public Formosa::Gramambular2::LanguageModel {
132+
public:
133+
explicit SimpleLM(const char* input, bool readingIsFirstColumn = true) {
134+
std::stringstream sstream(input);
135+
while (sstream.good()) {
136+
std::string line;
137+
getline(sstream, line);
138+
if (line.empty() || line[0] == '#') {
139+
continue;
140+
}
141+
std::stringstream linestream(line);
142+
std::string col0;
143+
std::string col1;
144+
std::string col2;
145+
linestream >> col0;
146+
linestream >> col1;
147+
linestream >> col2;
148+
db_[readingIsFirstColumn ? col0 : col1].emplace_back(
149+
readingIsFirstColumn ? col1 : col0, std::stod(col2));
150+
}
151+
}
152+
153+
std::vector<Unigram> getUnigrams(const std::string& key) override {
154+
const auto f = db_.find(key);
155+
return f == db_.end() ? std::vector<Unigram>() : (*f).second;
156+
}
157+
158+
bool hasUnigrams(const std::string& key) override {
159+
return db_.find(key) != db_.end();
160+
}
161+
162+
protected:
163+
std::map<std::string, std::vector<Unigram>> db_;
164+
};
165+
166+
TEST(UserOverrideModelTest, WalkResultSnapshotTest) {
167+
// See https://github.com/openvanilla/McBopomofo/issues/885.
168+
// This is a kind of integration test for the following steps:
169+
// 1. Type ㄐㄧ ㄧㄡˊ -> 機由; this assumes P(機)P(由) > P(機油)
170+
// 2. Override with 機油 and tell UOM to observe
171+
// 3. Reset, type ㄐㄧ ㄧㄡˊ -> 機油 -- UOM should suggest the expected
172+
// override
173+
// 4. Reset, type ㄐㄧ -> 機
174+
// 5. Override with 積 and tell UOM to observe
175+
// 6. Reset, type ㄐㄧ ㄧㄡˊ -> 積由 -- this is expected due to UOM suggestion
176+
// 7. Override with 機油 and tell UOM to observe
177+
// 8. Reset, type ㄐㄧ -> 積 due to UOM suggestion
178+
// 9. Continue to type 由 -> walk result should be 積由, and the UOM should
179+
// suggest 機油 as the override candidate.
180+
181+
std::string sampleData(kSampleData);
182+
Formosa::Gramambular2::ReadingGrid grid(
183+
std::make_shared<SimpleLM>(sampleData.c_str()));
184+
UserOverrideModel uom(kCapacity, kHalflife);
185+
Formosa::Gramambular2::ReadingGrid::WalkResult walkBefore;
186+
Formosa::Gramambular2::ReadingGrid::WalkResult walkLatest;
187+
UserOverrideModel::Suggestion suggestion;
188+
std::vector<Formosa::Gramambular2::ReadingGrid::NodePtr>::const_iterator
189+
nodeIter;
190+
double timestamp = kFakeNow;
191+
192+
grid.insertReading("ㄐㄧ");
193+
grid.insertReading("ㄧㄡˊ");
194+
walkBefore = grid.walk();
195+
ASSERT_EQ(walkBefore.valuesAsStrings(),
196+
(std::vector<std::string>{"", ""}));
197+
198+
grid.overrideCandidate(1, "機油");
199+
walkLatest = grid.walk();
200+
ASSERT_EQ(walkLatest.valuesAsStrings(), (std::vector<std::string>{"機油"}));
201+
202+
nodeIter = walkLatest.findNodeAt(1, /*outCursorPastNode=*/nullptr);
203+
ASSERT_NE(nodeIter, walkLatest.nodes.cend());
204+
uom.observe(walkBefore, walkLatest, 1, timestamp);
205+
timestamp += 1.0;
206+
207+
grid.clear();
208+
grid.insertReading("ㄐㄧ");
209+
grid.insertReading("ㄧㄡˊ");
210+
walkLatest = grid.walk();
211+
suggestion = uom.suggest(walkLatest, 1, timestamp);
212+
timestamp += 1.0;
213+
ASSERT_EQ(suggestion.candidate, "機油");
214+
215+
grid.clear();
216+
grid.insertReading("ㄐㄧ");
217+
walkBefore = grid.walk();
218+
ASSERT_EQ(walkBefore.valuesAsStrings(), (std::vector<std::string>{""}));
219+
220+
grid.overrideCandidate(0, "");
221+
walkLatest = grid.walk();
222+
ASSERT_EQ(walkLatest.valuesAsStrings(), (std::vector<std::string>{""}));
223+
224+
nodeIter = walkLatest.findNodeAt(0, /*outCursorPastNode=*/nullptr);
225+
ASSERT_NE(nodeIter, walkLatest.nodes.cend());
226+
uom.observe(walkBefore, walkLatest, 0, timestamp);
227+
timestamp += 1.0;
228+
229+
grid.clear();
230+
grid.insertReading("ㄐㄧ");
231+
walkLatest = grid.walk();
232+
suggestion = uom.suggest(walkLatest, 0, timestamp);
233+
timestamp += 1.0;
234+
ASSERT_EQ(suggestion.candidate, "");
235+
236+
grid.overrideCandidate(0, "");
237+
grid.insertReading("ㄧㄡˊ");
238+
239+
// THIS MUST BE A COPY OF NODES, otherwise the observation below will not
240+
// capture the correct state of the grid.
241+
// See https://github.com/openvanilla/McBopomofo/issues/885.
242+
//
243+
// This is wrong:
244+
// walkBefore = grid.walk();
245+
//
246+
// The following is correct:
247+
walkBefore = grid.walk().copyWithFixedNodes();
248+
249+
ASSERT_EQ(walkBefore.valuesAsStrings(),
250+
(std::vector<std::string>{"", ""}));
251+
252+
grid.overrideCandidate(1, "機油");
253+
walkLatest = grid.walk();
254+
ASSERT_EQ(walkLatest.valuesAsStrings(), (std::vector<std::string>{"機油"}));
255+
256+
nodeIter = walkLatest.findNodeAt(1, /*outCursorPastNode=*/nullptr);
257+
ASSERT_NE(nodeIter, walkLatest.nodes.cend());
258+
uom.observe(walkBefore, walkLatest, 1, timestamp);
259+
timestamp += 1.0;
260+
261+
grid.clear();
262+
grid.insertReading("ㄐㄧ");
263+
walkLatest = grid.walk();
264+
suggestion = uom.suggest(walkLatest, 0, timestamp);
265+
timestamp += 1.0;
266+
ASSERT_EQ(suggestion.candidate, "");
267+
grid.overrideCandidate(0, "");
268+
grid.insertReading("ㄧㄡˊ");
269+
walkLatest = grid.walk();
270+
ASSERT_EQ(walkLatest.valuesAsStrings(),
271+
(std::vector<std::string>{"", ""}));
272+
273+
suggestion = uom.suggest(walkLatest, 1, timestamp);
274+
timestamp += 1.0;
275+
ASSERT_EQ(suggestion.candidate, "機油");
276+
}
277+
122278
} // namespace McBopomofo

src/Engine/gramambular2/reading_grid.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,24 @@ class ReadingGrid {
178178

179179
std::vector<std::string> valuesAsStrings() const;
180180
std::vector<std::string> readingsAsStrings() const;
181+
182+
// Makes a copy with the nodes also being copies instead of refernces to
183+
// those in the current grid.
184+
//
185+
// For performance reasons, nodes is a vector of shared ptrs to those
186+
// nodes in the referenced grid. If the intent is to have the walk capture
187+
// the current state of the grid before the grid mutates, the default
188+
// behavior will not be enough. Instead, use this to make sure that the
189+
// nodes are correctly copied.
190+
WalkResult copyWithFixedNodes() const {
191+
std::vector<NodePtr> copiedNodes;
192+
for (const auto& n : nodes) {
193+
copiedNodes.push_back(std::make_shared<Node>(*n));
194+
}
195+
196+
return WalkResult{copiedNodes, totalReadings, vertices, edges,
197+
elapsedMicroseconds};
198+
}
181199
};
182200

183201
WalkResult walk();

src/KeyHandler.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1780,11 +1780,18 @@ void KeyHandler::pinNode(
17801780
size_t actualCursor = actualCandidateCursorIndex();
17811781
Formosa::Gramambular2::ReadingGrid::Candidate gridCandidate(
17821782
candidate.reading, candidate.value, "");
1783+
1784+
// Since WalkResult makes references to the current nodes, we must make a
1785+
// copy of the walk that *has a copy* of the current nodes to capture the
1786+
// current state. ReadingGrid::overrideCandidate() changes the state, and
1787+
// so having a simple copy of latestWalk_ (auto prevWalk = latestWalk_;)
1788+
// is NOT enough.
1789+
Formosa::Gramambular2::ReadingGrid::WalkResult prevWalk =
1790+
latestWalk_.copyWithFixedNodes();
17831791
if (!grid_.overrideCandidate(actualCursor, gridCandidate)) {
17841792
return;
17851793
}
17861794

1787-
Formosa::Gramambular2::ReadingGrid::WalkResult prevWalk = latestWalk_;
17881795
walk();
17891796

17901797
// Update the user override model if warranted.

0 commit comments

Comments
 (0)