Sudoku: What Happened?
Before we plug in the new Techniques, let’s work on the ‘Making App’ with a very small Logger. Very mediocre results this morning. Meh.
Every time I want to log some new information, all I have to do is:
- Add a class variable to whatever class wants to log the info;
- In that class, increment the variable;
- Where I want log info, typically in a test,
- First add a line to clear the variable by name;
- Then add a line to print the result.
I think we can smooth this out a bit, quickly, and that will make it easier to improve the information we can get as we improve our Techniques, thus, we hope, improving our solving performance.
I propose a small class, Logger, with a class variable instance maybe named log, and a method count that takes a string. A method to clear and a method to print. Let’s TDD it, just to get in the habit.
class TestLogger:
def test_hookup(self):
assert 2 + 1 == 4
Fails as intended.
def test_logger(self):
Logger.initialize()
Logger.log.count("Test")
assert Logger.log.report == "Test: 1"
There appears to be no such class as Logger. Quite the surprise. I “just” code up the whole thing:
class Logger:
log = None
@classmethod
def initialize(cls):
cls.log = cls()
def __init__(self):
if not hasattr(self, 'info'):
self.info = dict()
def count(self, name):
try:
self.info[name] += 1
except KeyError:
self.info[name] = 1
@property
def report(self):
lines = [f'{name}: {value:d}' for name, value in sorted(self.info.items())]
return '\n'.join(lines)
The word “just” up there is bearing a lot of weight. What really happened includes:
- I wanted not to need
initialize, forLogger.logto just work. If there is a clean way to do that, I don’t know it. - Along the way I found and scanned at least three articles on Python singletons,
- And then learned about
__new__ - And tried several different forms such as
Logger().count. - And now I don’t like any of my solutions, not even this one. Let’s add another test, because what I want already works:
def test_log_two_items(self):
Logger.initialize()
Logger.log.count("B")
Logger.log.count("A")
Logger.log.count("A")
assert Logger.log.report == "A: 2\nB: 1"
Green. Commit: Initial Logger class somewhat satisfactory.
Move Logger to its own file. Green. Commit: Move Logger to own file.
Change our 9x9 test to use it … I get this far …
def test_first_9x9_with_technique(self):
assert self.puzzle_list == self.puzzle_list_2 # ensuring no typos
puzzle = Puzzle.from_list(self.puzzle_list)
Logger.initialize()
solver = Solver(puzzle)
solved_puzzle = solver.solve()
assert solved_puzzle.is_correctly_solved
print(Logger.log.report)
assert Solver.solver_count < 800
assert SudokuTechniques.only_one < 15000
When will I learn not to create new objects from from an idea in my head? They never really quite fit my needs when I do that!
I want to be able to fetch values and assert on them. Let’s just say what we want and sort it out.
def test_first_9x9_with_technique(self):
assert self.puzzle_list == self.puzzle_list_2 # ensuring no typos
puzzle = Puzzle.from_list(self.puzzle_list)
Logger.initialize()
solver = Solver(puzzle)
solved_puzzle = solver.solve()
assert solved_puzzle.is_correctly_solved
print(Logger.log.report)
log = Logger.log
assert log["Solver Count"] < 800
assert log["Technique only_one"] < 15000
This fails, of course, because log is not subscriptable. Yet.
class Logger:
def __getitem__(self, item):
try:
return self.info[item]
except KeyError:
return ""
Now the test fails thus:
> assert log["Solver Count"] < 800
E TypeError: '<' not supported between instances of 'str' and 'int'
Ah, good point, we are just logging counts, so we could force int in Logger. But that will not long endure, so:
assert int(log["Solver Count"]) < 800
assert int(log["Technique only_one"]) < 15000
That fails because ‘’ does not turn into an int. So now, at last …
class Solver:
def __init__(self, puzzle):
Logger.log.count("Solver Count")
self.puzzle = puzzle
Two! tests fail. A, the 3x3 also checks solver count, we’ll ignore that for now.
9x9 now wants “Technique only_one”. Provide the corresponding stuff in SudokuTechniques and now three (!) tests fail. This is not what I’d usually call progress.
Oh darn. If I do unconditional logging in the methods, then the log variable must be initialized. We’re either going to have to jump through our own orifice or say .log() instead of .log. I choose the latter.
I wish I had realized this sooner. At this point there isn’t much to do but move forward. Shouldn’t be too awful.
class Logger:
_log = None
@classmethod
def initialize(cls):
cls._log = cls()
@classmethod
def log(cls):
if cls._log is None:
cls._log = cls()
return cls._log
Then find all the .log and fix them. After only a little hassle, this runs:
def test_first_9x9_with_technique(self):
assert self.puzzle_list == self.puzzle_list_2 # ensuring no typos
puzzle = Puzzle.from_list(self.puzzle_list)
Logger.initialize()
solver = Solver(puzzle)
solved_puzzle = solver.solve()
assert solved_puzzle.is_correctly_solved
print(Logger.log().report)
log = Logger.log()
assert int(log["Solver Count"]) < 800
assert int(log["Techniques only_one"]) < 15000
And the other tests also run. We get this report from the 9x9:
Puzzle Guesses: 18
Solver Count: 14
Techniques only_one: 6
Techniques only_one_position: 420
The Puzzle guesses tally adds more than one at a time. I’m not sure that I like that but to make it work:
class Logger:
def count(self, name, number=1):
try:
self.info[name] += number
except KeyError:
self.info[name] = number
Green. Commit: now using Logger exclusively.
And let’s reflect.
Reflection
The “big” mistake, I think was in not actually thinking about and testing the Logger in the fashion it was going to be used. Instead, I only just thought of counting things, not so much asserting on them.
As it stands, it’s doing the job … and it is a pain to use. To log something we have to say
Logger.log().count("Some Thing")
That’s at least one dot too many. To assert on a value, which so far are just integers we say:
assert int(Logger.log()["Techniques only_one"]) < 15000
And that is really a pain. We save a bit of pain by caching Logger.log() but it’s still not what I’d call fun. I’ll need to do some studying of Python patterns, especially maybe Singleton (arrgh) to see how we might improve this.
For now, it’s doing what we need, and isn’t terribly awkward in the production classes, with a bit more hassle in the tests. We don’t need much so let’s ride with this and see what we get.
Moving On
Now … remembering that we were here to drain the swamp, let’s see if we can put NakedPairs into the techniques.
class NakedPairsTechnique:
def __init__(self, puzzle, component):
self.puzzle = puzzle
self.component = component
def apply(self):
def cond(p1, p2):
return len(p1) == 2 and p1 == p2
for naked_pair in self.component.find_candidates_pairs(cond):
self.puzzle = self.component.remove_naked_pair(naked_pair)
return self.puzzle
Bummer, this needs to be done for every component. Well, OK, let’s do it in SudokuTechniques:
def naked_pairs(self):
changed = False
components = self.puzzle.all_components()
for component in components:
np = NakedPairsTechnique(self.puzzle, component)
changed |= np.apply()
return changed
We aren’t returning a changed flag from NP, so I rig that up … and, I’ll spare you some code, the tests loop.
Roll back. Something wrong with NakedPairs.
Let’s not give up, let’s try the HiddenPairsTechnique instead.
Again I’ll spare you the code. That loops also. My hypothesis is that both these techniques return as changed when in fact nothing was changed. I’ll have to verify that, but I’m rather sure that I implemented the change feature correctly both times. What I did not do was to test it directly in the tests for those two classes.
I’ll need to do that. For now roll back again.
Summary
What is today’s Big Lesson? It is to take smaller steps. In the cases of trying to install the NakedPairs and HiddenPairs techniques, I’m pretty sure (0.7+) that they are returning a True changed flag when they should not have. Right or wrong, that needs testing. And NakedPairs wasn’t even ready to be used as a Technique, since it only checks one component, not the whole puzzle. I think it’s OK to have a Technique aimed at a component, and then to have another class or method that applies it otherwise, but our platform isn’t ready for that way of working.
And arguably the Logger results are also calling for smaller steps, though, there, I think the steps taken were perhaps small enough, we just needed a few more tests before locking in on how the thing worked.
Overall, well, we have somewhat better logging, in that it is centralized and any object can now just log any counts it wants, without setting up class variables. And the report will automatically produce all the logged values, in alphabetic order.
So that’s nice.
I give the morning about a B- or C+. Not the best, but not the worst either.
Next time … more testing on Naked and Hidden Pairs, with an eye to the looping. My hypothesis, slightly more detailed, is that both those objects are identifying cases that have already been reduced as if they need reduction. They find the pairs … but do not realize that the corresponding adjustment has already been made.
That’s a guess and it could be wrong.
We’ll find out … next time! For now, it’s break and chai time. See you soon!