changeset 25:0bbeb01a7681

combine from various sources
author Henry Thompson <ht@markup.co.uk>
date Sat, 29 May 2021 21:32:41 +0100
parents 6df2f6dcc809 (current diff) 69a494ef1a58 (diff)
children 5488b5d3ba10 6c389c0f1b40
files
diffstat 3 files changed, 490 insertions(+), 148 deletions(-) [+]
line wrap: on
line diff
--- a/mailer.py	Sat May 29 21:29:08 2021 +0100
+++ b/mailer.py	Sat May 29 21:32:41 2021 +0100
@@ -1,6 +1,6 @@
-#!/usr/bin/python
+#!/usr/bin/python2.7
 '''Attempt at flexible mailout functionality
-Usage: mailer.py [-n] [-s] [-C cc string] [-c COLSPEC[,COLSPEC]*] [-B bcc string] [-b COLSPEC[,COLSPEC]*] [-S col[,col]*] [-a COLSPEC[,COLSPEC]*] [-p COLPAT]* [-SA file[,file]*] [-R reply-to] COLSPEC[,COLSPEC]* subject {addr-file|-} body-file
+Usage: mailer.py [-n] [-s] [-C cc string] [-c COLSPEC[,COLSPEC]*] [-B bcc string] [-b COLSPEC[,COLSPEC]*] [-S col[,col]*] [-a COLSPEC[,COLSPEC]*] [-p COLPAT]* [-SA file[,file]*] [-R reply-to] [-f from] [-m .sigfilename] COLSPEC[,COLSPEC]* subject {addr-file|-} body-file
 
 Sends the body as a message from me with subject to destinations per
 lines in the addr-file selected by COLSPECs (to:) or -c/-b COLSPECs (Cc:/Bcc:)
@@ -18,6 +18,8 @@
 -S for columns to substitute as such
 -p for augmentation pattern for a column
 -R for replyTo address
+-f for fromName, defaults to $USER@$HOSTNAME
+-m for .sig file, None for none
 
 COLSPEC is of the form a[:n[:f[:g]]] selects from addr-file, which must be tsv
   a gives the column for an email address
@@ -47,7 +49,7 @@
 
 All column indices are 1-origin, as for cut'''
 
-import smtplib, sys, re, os.path, codecs
+import smtplib, sys, re, os, os.path, codecs
 from email.mime.text import MIMEText
 
 addrPat=re.compile("<([^>]*)>")
@@ -249,6 +251,8 @@
 
 dryrun=False
 replyTo=None
+sigfile=None
+fromName='%s@%s'%(os.getenv('USER'),os.getenv('HOSTNAME'))
 
 sys.argv.pop(0)
 doSub=False
@@ -328,6 +332,18 @@
       replyTo=sys.argv.pop(0)
     else:
       usage('reply to')
+  elif sys.argv[0]=='-f':
+    sys.argv.pop(0)
+    if sys.argv:
+      fromName=sys.argv.pop(0)
+    else:
+      usage('from name')
+  elif sys.argv[0]=='-m':
+    sys.argv.pop(0)
+    if sys.argv:
+      sigfile=sys.argv.pop(0)
+    else:
+      usage('sig file name')
   elif sys.argv[0][0]=='-':
     print sys.argv
     usage()
@@ -367,11 +383,16 @@
 else:
   usage('body')
 
-try:
-  sig=open("/home/ht/.signature","r")
-  signature=sig.read().rstrip()
-except:
-  signature=None
+signature=None
+if sigfile!='None':
+  if sigfile is None:
+    sigfile="/home/ht/.signature"
+  try:
+    sig=open(sigfile,"r")
+    signature=sig.read().rstrip()
+  except:
+    if sigfile!="/home/ht/.signature":
+      raise
 
 CS=', '
 body=bodyFile.read().rstrip()
@@ -421,8 +442,9 @@
   if dryrun:
     print recips
     print msg.keys()
+    print 'From: %s'%fromName
     print msg.as_string()
     exit()
   print "mailing to %s"%recips
-  mailer.sendmail("ht@inf.ed.ac.uk",recips,msg.as_string())
+  mailer.sendmail(fromName,recips,msg.as_string())
 mailer.quit()
--- a/nono.py	Sat May 29 21:29:08 2021 +0100
+++ b/nono.py	Sat May 29 21:32:41 2021 +0100
@@ -25,65 +25,111 @@
   def __init__(self,n,m,runs):
     list.__init__(self,list(range(n)))
     self.n=n
-    self.runs=runs
+    self.m=m
+    self.margin0=0 # a run can start here, so if >0 then self[m0-1].val must be False
+    self.marginN=n-1 # a run can end here, so if <n-1 then self[mN+1].val ditto
+    self.runs=self.origRuns=runs
+    self.initialComplete=[]
+    self.finalComplete=[]
+    self.resetAllRuns()
+
+  def __repr__(self):
+    return "V@%s%s:%s"%(self.x,self.origRuns,list.__repr__(self))
+
+  def __str__(self):
+    return '%s|'%('|'.join(str(c) for c in self))
+
+  def myPrintSize(self):
+    return sum(len(str(run)) for run in self.runs)+len(self.runs)-1
+
+  def resetAllRuns(self):
     # compute the set of all possible layouts for runs
     self.rn=len(self.runs)
     rtot=sum(self.runs)
-    self.allRuns=list(self.seedList(0,0,0,
+    self.allRuns=list(self.seedList(0,0,self.margin0,self.marginN+1,
                                     sum(1+self.runs[k] for k in range(self.rn))-1))
     self.nar=len(self.allRuns)
 
-  def seedList(self,i,j,pos,runLen):
+  def seedList(self,i,j,pos0,posN,runLen):
     """
     :param i: starting skip before next run
     :type i: 0 if pos==0 else 1
     :param j: next run number
     :type j: int
-    :param pos: left margin
-    :type pos: int
+    :param pos0: left margin
+    :type pos0: int
+    :param posN: right margin
+    :type posN: int
     """
-    bound=self.n-(pos+runLen)+1
-    #dprint('s',i,j,pos,runLen,bound)
+    bound=posN-(pos0+runLen)+1
+    dprint('s',i,j,pos0,posN,runLen,bound)
     if j==self.rn:
         yield []
         return
     r=self.runs[j]
     for v in range(i,bound):
-      for sub in self.seedList(1,j+1,pos+v+r,runLen-(r+1)):
+      for sub in self.seedList(1,j+1,pos0+v+r,posN,runLen-(r+1)):
         yield [-v,r]+sub
 
-  def __repr__(self):
-    return "V@%s%s:%s"%(self.x,self.runs,list.__repr__(self))
-
-  def __str__(self):
-    return '%s|'%('|'.join(str(c) for c in self))
-
-  def step(self):
+  def step(self,pos):
+    if self.runs==[]:
+      return 0
     scratch=[0 if c.val is None else c.val for c in self]
+    wins=0
+    changed=0
     for k,runs in enumerate(self.allRuns):
-      dprint('=====pass %s======'%k)
-      self.onepass(0,self.n,scratch,runs.copy())
-    dprint(scratch)
+      if (self.m.loop,self.cLet,pos,k)==(1,'R',7,5):
+        print(self.m)
+      dprint('=====pass %s.%s.%s.%s======'%(self.m.loop,self.cLet,pos,k))
+      if self.onepass(self.margin0,self.marginN+1,scratch,runs.copy()):
+        wins+=1
+    dprint(wins, scratch)
+    maybeSeq=None
+    inSeq=None
+    j=None
     for i in range(self.n):
-      if scratch[i]==self.nar:
+      if scratch[i]>=wins:
         # If blobby in _every_ pass, then must be a blob
+        if inSeq is None:
+          inSeq=i if maybeSeq is None else maybeSeq
+          dprint('s1',inSeq)
+          maybeSeq=None
         if self[i].val is None:
-          self[i].setVal(True)
+          dprint('s1a',i)
+          self.newBlob(i,True)         # Check cross-vector
+          j=i
+          changed=1
         elif self[i].val is True:
-          # already there
           pass
         else:
-          print("Shouldn't happen: attempt to blob where x already present! %s at %s"%(self,i),file=sys.stderr)
-          exit(101)
+          eprint("Shouldn't happen: attempt to blob where x already present! %s at %s"%(self,i),err=101)
+      elif inSeq is not None:
+        if self[i].val is not True:
+          inSeq=None
+          maybeSeq=None
+          dprint('s2',i-1,j)
+          if j is not None:
+            # Only if we actually added a blob at some point
+            dpush('b_s2')
+            self.checkNew(i-1)
+            dpop('b_s2')
+      elif self[i].val is True and maybeSeq is None:
+        dprint('s3',i)
+        maybeSeq=i
+    dprint('s4',inSeq,i)
+    if inSeq is not None:
+      dpush('b_s4')
+      self.checkNew(i)
+      dpop('b_s4')
+    return changed
 
   def onepass(self,i0,iBound,scratch,stack):
     """note that stack is not a simple run, but one with _negative_ numbers between
      and possibly before the positive ones, indicating obligatory skips
      """
     i=i0 # starting index into self/scratch/maybe
-    j=-1 # index into run
-    maybe=[0]*iBound
-    dprint('r: %s'%stack)
+    maybe=[0]*self.n
+    dprint('r: %s'%stack,i0,iBound)
     req=sum((-r if r<0 else r) for r in stack)
     while stack and i<iBound:
       r=rr=stack.pop(0)
@@ -91,92 +137,52 @@
       if r<1:
         # obligatory skip
         # (Above init of self.allRuns is easier if we allow a 0 to be ignored
+        for k in range(i,i-r):
+          if self[k] is True:
+            # has to be False or None to skip
+            dprint('x0',k)
+            return False
         i-=r
         req+=r
         r=rr=stack.pop(0)
       # rr is run remaining -- how many we still need
-      j+=1 # index of current run in self.runs, we'll need to decorate that eventually
-      inOne=-1 # if non-neg, records the start point of a possible run
-      gapsFilled=0
       # First, check if we can start here:  0 is OK, and n>0 iff n-1 is None or False
-      if i>0 and i<iBound:
-        while self[i-1].val:
-          i+=1
+      # if i>0 and self[i-1].val is True:
+      #  dprint('x1',i)
+      #  return
       if (iBound-i)<req:
         # Can't win, give up altogether
-        dprint('c0',i,iBound,req)
-        return
-      while i<iBound:
+        dprint('x2',i,iBound,req)
+        return False
+      j=i # start of run, if we find it
+      gapsFilled=0
+      dprint('top:',j)
+      while rr>0:
+        # guaranteed by check above that we won't run over the far end
         c=self[i].val
-        dprint('top',i,c,inOne,rr)
+        if c is False:
+          dprint('x3',i)
+          return False
         if c is None:
           # we could add a blob here
-          dprint('c1')
           gapsFilled+=1
-          rr-=1
-          if inOne<0:
-            dprint('c1a',i)
-            # starts here
-            inOne=i
-          # fall through to check for completion
-        else:
-          dprint('c2')
-          # c is a bool
-          if inOne<0:
-            dprint('c2a')
-            if c:
-              dprint('c2a1')
-              # a *, we can possible start something here
-              inOne=i
-              rr-=1
-              # fall through to check for completion
-            else:
-              dprint('c2a2')
-              # an x, can't start here, just move along
-              i+=1
-              continue
-          else:
-            dprint('c2b')
-            if c:
-              dprint('c2b1')
-              # a blob, extend or complete a partial
-              rr-=1
-              # fall through to check for completion
-            else:
-              # abandon a partial
-              dprint('c2b2')
-              inOne=-1
-              rr=r
-              i+=1
-              continue
-        if rr>0:
-          dprint('c3')
-          # we're not done, carry on
-          i+=1
-          continue
-        # Maybe a win?
-        # look ahead, can we stop here?
-        # NB _self_.n
-        if i+1<self.n and self[i+1].val:
-          dprint('c4')
-          # Nope
-          inOne=-1
-          rr=r
-          gapsFilled=0
-          i+=1
-          continue
-        elif gapsFilled==0:
-          dprint('c5')
-          # We must have crossed at least on gap...
-          print("Shouldn't happen: no gap! me:%s i:%s j:%s rr:%s inOne:%s"%(self,i, j, rr, inOne),file=sys.stderr)
-          exit(100)
-        # Victory!
-        dprint('c6',r,inOne,i)
-        for k in range(inOne,i+1):
-          maybe[k]+=1
+        # and a blob already present is OK
+        rr-=1
         i+=1
-        req-=r
-        break
+      # Maybe a win?
+      # look ahead, can we stop here?
+      if i<self.n and self[i].val is True:
+        dprint('x4',i)
+        return False
+#       elif gapsFilled==0:
+#         # We must have crossed at least one gap...
+#         print("Shouldn't happen: no gap! me:%s i:%s j:%s rr:%s"%(self,i, j, rr),file=sys.stderr)
+#         raise Exception
+      # Victory!
+      dprint('c6',r,j,i)
+      for k in range(j,i):
+        maybe[k]+=1
+      req-=r
       # on to the next run
     # end of inner loop, did we win?
     if (not stack) or i==iBound:
@@ -184,29 +190,242 @@
       dprint('win:',maybe)
       for k in range(iBound):
         scratch[k]+=maybe[k]
+      return True
+    eprint("Shouldn't happen? - fell through",stack,i,iBound,err=102)
+
+  def checkX(self,pos,crosspos):
+    # New x at pos, are there others that can be xed out now?
+    # Overkill as is?
+    dprint('cx',self.cLet+str(crosspos),pos)
+    if self.runs:
+      start0=self.margin0 # 0 if self.margin0==0 else self.margin0+1
+      if self.marginal(range(start0,pos),self.runs[0]):
+        dprint('cx1a')
+      else:
+        dprint('cx1b')
+#    if len(self.runs)>1:
+      startN=self.marginN # self.marginN if (self.marginN==self.n-1) else self.marginN-1
+      if self.marginal(range(startN,pos,-1),self.runs[-1]):
+        dprint('cx2a')
+      else:
+        dprint('cx2b')
+
+  def checkNew(self,pos):
+    # New blob at pos, can we complete anything?
+    # Assuming @@ FTTB that it's never possible to definitively complete a
+    #  non-marginal run, which is wrong if the length is unique and it's bounded...
+    start0=self.margin0 #0 if self.margin0==0 else self.margin0+1
+    startN=self.marginN # self.marginN if (self.marginN==self.n-1) else self.marginN-1
+    dprint('cn',start0,pos,startN)
+    changed=False
+    # First, find our boundaries:
+    s=pos # start index of our run
+    if s>start0:
+      while s>start0 and self[s-1].val is True:
+        s-=1
+    f=pos # finish index of our run
+    if f<startN:
+      while f<startN and self[f+1].val is True:
+        f+=1
+    l=(f-s)+1 # our length
+    dprint('%s:%s,%s,%s,%s:<%s>'%(self.cLet,s,f,l,self.runs,self))
+    c=self.runs.count(l)
+    if c==0:
+      # not big enough yet
+      dprint('x0')
+      return
+    if self.runs[0]==l:
+      # is it safely left marginal, i.e. no blobs or big enough gaps before us?
+      if self.marginal(range(start0,s),l):
+        changed=True
+        dprint('n1')
+        # mark our margins
+        for i in range(start0,s): # not sure this is still needed since
+                                        # added gap-filling in self.marginal
+          if self[i].val is None:
+            self.newX(i,False)
+        if f<startN:
+          if self[f+1].val is None:
+            self.newX(f+1,True)
+        self.found0(f) # pull in the start margin at least to f+2
+      else:
+        dprint('x1a')
+    if not changed:
+      if self.runs[-1]==l:
+        # is it safely _right_ marginal, i.e. no blobs or big enough gaps _after_ us?
+        if self.marginal(range(startN,f,-1),l):
+          changed=True
+          dprint('n2')
+          # mark our margins: still needed? see above
+          for i in range(startN,f,-1):
+            if self[i].val is None:
+              self.newX(i,False)
+          if s>start0:
+            if self[s-1].val is None:
+              self.newX(s-1,True)
+          self.foundN(s) # pull in the finish margin at least to s-2
+        else:
+          dprint('x2a')
+      else:
+        dprint('x2b')
+    if changed:
+      self.resetAllRuns()
+
+  def marginal(self,rng,l):
+    dprint('m%s'%self.cLet,rng.start,rng.stop,rng.step,l)
+    g=0 # length of a gap
+    for i in rng:
+      if self[i].val is True:
+        # Shouldn't be possible?
+        dprint('mx0')
+        return False
+      if self[i].val is False:
+        if g>0:
+          # Block a too-small gap
+          dprint('m1',i-g,i)
+          for j in range(i-g,i):
+            self.newX(j,True)
+          g=0
+      else:
+        # None
+        g+=1
+        if g==l:
+          # run could fit here, so no go
+          dprint('mx1')
+          return False
+    if g>0:
+      # Block a too-small gap
+      if rng.step==1:
+        # forward
+        dprint('m2f',i-g,i)
+        for j in range(i+1-g,i+1):
+          self.newX(j,True)
+      else:
+        # backward
+        dprint('m2b',i+g,i)
+        for j in range(i+g-1,i-1,-1):
+          self.newX(j,True)
+    return True
+
+  def found0(self,i):
+    dprint('found0 called on %s at %s'%(self,i))
+    i=self.margin0
+    while self[i].val is False:
+      i+=1
+    if self[i].val is True:
+      r=self.runs.pop(0)
+      self.initialComplete.append(r)
+      self.margin0+=r+1
+      self.updateHeader(r=r,pre=True)
+
+  def foundN(self,i):
+    dprint('foundN called on %s at %s'%(self,i))
+    i=self.marginN
+    while self[i].val is False:
+      i-=1
+    if self[i].val is True:
+      r=self.runs.pop()
+      self.finalComplete=[r]+self.finalComplete
+      self.marginN-=r+1
+      self.updateHeader(r=r,pre=False)
+    
 
 class Row(Vector):
-  def __init__(self,n,m,runs,pos,dprintWidth):
+  cLet='R'
+  def __init__(self,n,m,runs,pos):
     Vector.__init__(self,n,m,runs)
     self.y=pos
-    self.dprintWidth=dprintWidth
-    self.fmt="%%%ss|"%dprintWidth
+    self.width=self.myPrintSize()
 
   def __str__(self):
     return ((self.fmt%(' '.join(str(r) for r in self.runs)))+
             Vector.__str__(self))
 
+  def updateHeader(self,*,maxWidth=None,r=None,pre=None):
+    if maxWidth is None:
+      # update
+      spacer=(" " if self.runs else "")
+      if pre:
+        self.infix+=(RedFmt%r)+spacer
+      else:
+        # post
+        self.suffix=spacer+RedFmt%r+self.suffix
+      self.fmt="%s%s%%s%s|"%(self.prespace,self.infix,self.suffix)
+    else:
+      # init
+      self.maxWidth=maxWidth
+      self.prespace=' '*(maxWidth-self.width)
+      self.fmt="%s%%s|"%self.prespace
+      self.infix=""
+      self.suffix=""
+
+  def newBlob(self,x,crossCheck=False):
+    self[x].setVal(True)
+    if crossCheck:
+      dpush('b_cc')
+      self[x].column.checkNew(self.y)
+      dpop('b_cc')
+
+  def newX(self,x,crossCheck=False):
+    dprint('nx %s%s@%s'%('R',self.y,x))
+    self[x].setVal(False)
+    if crossCheck:
+      dpush('x_cc')
+      self[x].column.checkX(self.y,x)
+      dpop('x_cc')
+
 class Column(Vector):
-  def __init__(self,n,m,runs,pos,dprintHeight):
+  cLet='C'
+  def __init__(self,n,m,runs,pos):
     Vector.__init__(self,n,m,runs)
     self.x=pos
-    self.dprintHeight=dprintHeight
-    self.fmt="%%%ss"%self.dprintHeight
-    self.updateHeader()
+    self.height=self.myPrintSize()
 
-  def updateHeader(self):
-    header=('-'.join(str(c) for c in self.runs))
-    self.header=self.fmt%header # pad to same 'height'
+  def updateHeader(self,*,maxHeight=None,r=None,pre=None):
+    dprint('CuH',r,pre)
+    if maxHeight is None:
+      # update
+      if pre:
+        for rc in str(r):
+          self.infix.append(RedFmt%rc)
+        if self.runs:
+          self.infix.append('-')
+      else:
+        # post
+        ins=["-"] if self.runs else []
+        for rc in str(r):
+          ins.append(RedFmt%r)
+        self.suffix=ins+self.suffix
+      dprint('CuH1: |%s|,%s,%s,%s'%(self.prespace,self.infix,self.suffix,self.runs))
+      self.header=([" "]*self.prespace)+\
+                   self.infix+\
+                   (['-'.join(str(c) for c in self.runs)] if self.runs else [])+\
+                   self.suffix
+    else:
+      # init
+      self.maxHeight=maxHeight
+      self.infix=[]
+      self.suffix=[]
+      self.prespace=maxHeight - self.height # pad to same 'height'
+      self.fmt="%s%%s"%(' '*self.prespace)
+      header=('-'.join(str(c) for c in self.runs))
+      self.header=self.fmt%header
+    dprint(self.header)
+
+  def newBlob(self,y,crossCheck=False):
+    self[y].setVal(True)
+    if crossCheck:
+      dpush('b_cc')
+      self[y].row.checkNew(self.x)
+      dpop('b_cc')
+
+  def newX(self,y,crossCheck=False):
+    dprint('nx %s%s@%s'%('C',self.x,y))
+    self[y].setVal(False)
+    if crossCheck:
+      dpush('x_cc')
+      self[y].row.checkX(self.x,y)
+      dpop('x_cc')
 
 class Cell:
   def __init__(self,row,y,column,x):
@@ -228,43 +447,88 @@
   def setVal(self,v):
     if v is True:
       if self.val is False:
-        dprint("Warning: x -> * at %s,%s"%(self.x,self.y))
+        wprint("Warning: x -> * at %s,%s"%(self.x,self.y))
       elif self.val is True:
         # No-op
         return
-      # @@ check row/col completed
+      self.val=v
     else:
       if self.val is not None:
-        dprint("Warning: %s -> %s at %s,%s"%(self.val,v,self.x,self.y))
-    self.val=v
+        wprint("Warning: %s -> %s at %s,%s"%(self.val,v,self.x,self.y))
+      self.val=v
+        
 
 class Nono(dict):
   # 0,0 is upper left, so increasing y goes _downwards_, to match the standard layout
-  def __init__(self,rows,cols):
-    n=self.n=len(cols)
-    if n!=len(rows):
-      print("losing r:%s x c:%s"%(len(rows),n),sys.stderr)
+  def __init__(self,runsPerRow,runsPerCol):
+    global SOLVER
+    self.loop=0
+    self.dp=''
+    self.dstate=[]
+    SOLVER=self
+    n=self.n=len(runsPerCol)
+    if n!=len(runsPerRow):
+      print("losing r:%s x c:%s"%(len(runsPerRow),n),sys.stderr)
       exit(1)
-    self.rc=rows
-    rowDprintWidth=max(sum(len(str(r)) for r in row)+len(row)-1 for row in rows)
-    self.rowfmt="%s|%%s"%(' '*rowDprintWidth)
-    self.cc=cols
-    # dprint col nums>9 vertically :-(
-    self.colDprintHeight=max(sum(len(str(c)) for c in col)+len(col)-1 for col in cols)
-    self.columns=cc=[Column(n,self,cols[i],i,self.colDprintHeight) for i in range(20)]
-    self.rows=rr=[Row(n,self,rows[i],i,rowDprintWidth) for i in range(20)]
-    for x in range(20):
-      for y in range(20):
+    self.rc=runsPerRow
+    self.cc=runsPerCol
+    # print col nums>9 vertically :-(
+    self.columns=cc=[Column(n,self,runsPerCol[i],i) for i in range(n)]
+    self.maxCRheight=maxCRheight=max(col.height for col in cc)
+    for c in cc:
+      c.updateHeader(maxHeight=maxCRheight)
+    self.rows=rr=[Row(n,self,runsPerRow[i],i) for i in range(n)]
+    maxRRwidth=max(row.width for row in rr)
+    for r in rr:
+      r.updateHeader(maxWidth=maxRRwidth)
+    self.rowfmt="%s|%%s|"%(' '*maxRRwidth)
+    for x in range(n):
+      for y in range(n):
         self[(x,y)]=Cell(rr[y],y,cc[x],x)
 
   def __str__(self):
     lines=[self.rowfmt%('|'.join([(self.columns[i]).header[j] for i in range(self.n)])) # 'rotate'
-           for j in range(self.colDprintHeight)]
+           for j in range(self.maxCRheight)]
     lines+=[str(r) for r in self.rows]
     return "\n".join(lines)
 
+  def solve(self):
+    someChanged=1
+    while someChanged>0:
+      self.loop+=1
+      someChanged=0
+      dprint("***Solve C %s***"%self.loop)
+      for c in self.columns:
+        someChanged+=c.step(c.x)
+      print(someChanged)
+      print(self)
+      dprint("***Solve R %s***"%self.loop)
+      for r in self.rows:
+        someChanged+=r.step(r.y)
+      print(someChanged)
+      print(self)
+
+def dpush(s):
+  SOLVER.dp+=' '
+  SOLVER.dstate.append(s)
+
+def dpop(s):
+  assert(SOLVER.dstate.pop()==s)
+  SOLVER.dp=SOLVER.dp[1:]
+
 def dprint(*args):
-  pass
+  print(SOLVER.dp,end='')
+  print(*args)
+  sys.stdout.flush()
+
+def eprint(*args,**kw):
+  print(*args,file=sys.stderr)
+  sys.stderr.flush()
+  exit(kw['err'])
+
+def wprint(*args):
+  print(*args,file=sys.stderr)
+  sys.stderr.flush()
 
 if __name__ == '__main__':
   if len(sys.argv)>1:
@@ -287,12 +551,4 @@
   rows=[[int(s) for s in l.split()] for l in f]
 
   solver=Nono(rows,cols)
-  print(solver)
-  for c in solver.columns:
-    c.step()
-  print()
-  print(solver)
-  for r in solver.rows:
-    r.step()
-  print()
-  print(solver)
+  solver.solve()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/xldiff.py	Sat May 29 21:32:41 2021 +0100
@@ -0,0 +1,64 @@
+#!/usr/bin/python3
+'''Compare two excel books
+   Courtesy of https://kanoki.org/2019/02/26/compare-two-excel-files-for-difference-using-python/'''
+import pandas as pd
+import numpy as np
+import math, sys
+def usage():
+  print('''Usage: xldiff.py f1 f2''',file=sys.stderr)
+
+if len(sys.argv)==3:
+  f1=sys.argv[1]
+  f2=sys.argv[2]
+  try:
+    with open(f1) as _:
+      pass
+  except:
+    print("Can't open %s"%f1,file=sys.stderr)
+    exit(2)
+  try:
+    with open(f2) as _:
+      pass
+  except:
+    print("Can't open %s"%f2,file=sys.stderr)
+    exit(2)
+else:
+  usage()
+  exit(1)
+
+def check(s1,s2):
+  lv=(s1.columns==s2.columns)
+  ii=np.where(lv==False)[0]
+  for i in ii:
+    if not(pd.isnull(s1.columns[i]) and
+           pd.isnull(s2.columns[i])):
+      print('',"0,%s: |%s| ~= |%s|"%(i,
+                                   s1.columns[i],
+                                   s2.columns[i]))
+  cv=(s1.values==s2.values)
+  rows,cols=np.where(cv==False)
+  for i,j in zip(rows,cols):
+    if not(pd.isnull(s1.iloc[i,j]) and
+           pd.isnull(s2.iloc[i,j])):
+      print('',"%s,%s: |%s| ~= |%s|"%(i+1,j,
+                                      s1.iloc[i,j],
+                                      s2.iloc[i,j]))
+
+b1=pd.read_excel(f1,None)
+b2=pd.read_excel(f2,None)
+
+keys=set(b1.keys()).union(set(b2.keys()))
+for k in keys:
+  if k not in b2:
+    print("Sheet %s in %s but not in %s"%(k,f1,f2))
+    continue
+  if k not in b1:
+    print("Sheet %s in %s but not in %s"%(k,f2,f1))
+    continue
+  s1=b1[k]
+  s2=b2[k]
+  print(k)
+  if not s1.equals(s2):
+    print(" (Sheet not of equal 'shape', may be false positive)")
+  check(s1,s2)
+