I've written these two command-line tools to be used to facilitate forestry surveys.
For a little background, the idea here is that often a landowner needs to know precisely how much wood is coming out of his dirt. Not only that, but he has to account for the fact that you can't turn a cylinder (trees) into rectangular prisms (boards) without a bit of loss.
So what the landowner does is he pays my client to come down and run around the woods with a bit of measuring tape, taking the circumferences of trees. He then apparently works out the radius in his head (I hope that's what he does, otherwise everything he's ever done is completely wrong) and shouts it into a radio along with the species and how tall he believes the tree is (in 16-foot logs). Sometimes, such as in the case of a tree that has two trunks, he'll call out more than one of the same sort of tree at one time.
The computer operator - that's me - then types a three-part statement into the computer containing a short arbitrary string that identifies the tree species ("WO" for White Oak, "ASH" for Ash, "YGG" for Yggdrasil, etc...), the diameter, and the length in 16-foot logs or halves thereof. If the forester called in more than one of the same kind of tree, I can also add a number on the end that tells the program how many of the specified tree I want to add.
This is all saved to disk. Once we're back at home base, we can take the data file the tallying program produced and feed it into the analysis program, which computes (using a formula derived from the original table the client used to do this process by hand, itself derived from arcane magics) the usable board footage of each variety of tree and lists the total board footage, total number of trees, board footage by species, board footage by species and size (one log that the sawmill can produce two boards from is worth more than two boards the 'mill can produce one from), etc. etc.
The client was very (indeed, quite irrationally) concerned about the reliability of the program, so I decided the best way to store the data was to keep it in memory for as short a time as possible, and instead save the tree data to disk every time a new tree was entered. That way, if there's a program crash or power failure or the computer's thermal safety goes off, nothing is lost (with the cost of a slight increase in the risk that the power will go out while the machine is writing to the disk, thus blanking/trashing the file). Eventually I might switch to a journaling system or Python's sqlite implementation, but at design-time I decided that I'd be better off with my own simple system, with the option of converting later on if it proves to be a good idea.
I've never really showed my code to anyone before, certainly never anyone who knew what they were doing, so I'm excited to see what my comrades think of my first "professional" bit of work!
This is the tallying program that actually takes in the tree data:
''' Created on Nov 2, 2013 @author: Schilcote ''' def load_trees(): trees=dict() try: with open("trees.txt","r") as treefil: while True: lin=treefil.readline() if lin=="": break species, count = lin.split() trees[species]=int(count) except FileNotFoundError: return dict() return trees def save_trees(trees): with open("trees.txt","w") as treefil: for thetree in trees: treefil.write(thetree+" "+str(trees[thetree])+"\n") return def main(): print("Autotally V0") print("Ready") while True: cmd=input(">").upper() cmdtup=list(cmd.split()) print (cmdtup) if cmdtup=="LIST": print(load_trees()) else: trees=load_trees() if len(cmdtup) >= 3: cmdtup.append(1) key=cmdtup+"_"+cmdtup+"_"+cmdtup try: trees[key]=int(trees[key])+int(cmdtup) except KeyError: trees[key]=int(cmdtup) try: print("Inserted "+str(cmdtup)+" "+str(cmdtup)+" of diameter "+str(cmdtup)+" with "+str(cmdtup)+" logs.") except IndexError: print("Inserted "+str(cmdtup)+" of diameter "+str(cmdtup)+" with "+str(cmdtup)+" logs.") save_trees(trees) if __name__ == '__main__': main()
And this is the analysis side of it:
''' Created on May 31, 2014 @author: Schilcote ''' import collections import pprint def scribner(diameter,length): "Convert length and diameter to board-feet via the Scribner method. WARNING: Only valid for measurements at top of tree. Do not use." a = (0.0494 * diameter * diameter * length) b = (0.124 * diameter * length) c = (0.269 * length) return a-b-c def regressive_scribner(d,l): "Convert length (in feet) and diameter (in inches) to board-feet via an equation derived from the Scribner tables. Accurate to around 10 board feet." v = 0.0942919095863512*d**2 + 0.0231348479474668*l*d**2 - 16.494587251523 - 0.119077488412871*l - 0.00210681861605682*d*l**2 return v def tree_to_dl(tree): "Takes a string tree representation and turns it into a tuple of (species, diameter, logs)" species, diameter, logs = tree.split("_") diameter=int(diameter) logs=float(logs) return species, diameter, logs from treetally import load_trees if __name__ == '__main__': trees=load_trees() totaltrees=sum(trees.values()) print("# of trees:",totaltrees) #We want all these dicts to start out with values of zero for every type of tree; #so we set their default_factory to a lambda that simply returns 0 retzero=lambda: 0 species_counts=collections.defaultdict(retzero) spec_diam_counts=collections.defaultdict(retzero) species_bf=collections.defaultdict(retzero) spec_diam_bf=collections.defaultdict(retzero) for tree, count in trees.items(): spec, diam, logs = tree_to_dl(tree) species_counts[spec]+=count spec_diam_counts[(spec,diam)]+=count bf=scribner(diam,int(logs*16)) #Scribner wants feet; 16 feet to a log species_bf[spec]+=int(bf)*count spec_diam_bf[(spec, diam)]+=int(bf)*count totalbf=int(sum(species_bf.values())) print("Total board feet:",totalbf) print("Avg. BF per tree: ",(totalbf//totaltrees)) print("Board feet by species:") pprint.pprint(species_bf) print("Board feet by species & diameter:") pprint.pprint(spec_diam_bf) print("Counts by species:") pprint.pprint(species_counts) print("Counts by species and diameter:") pprint.pprint(spec_diam_counts)
A few notes on the criticisms I anticipate:
- Yes, it's not compliant with PEP8. I don't find PEP8 to be particularly pleasant to write or read, however, so pleh. This code isn't intended to be worked on by anyone but myself, so I just did what I find the most readable.
- I probably shouldn't leave disused functions just lying around. The failed
scribner()was the product of hours of research, however, so I decided I ought to keep it around in the conceivable case I worked out a more elegant method than my regression-based formula.
- I realize that hardcoding the filename is unpythonic and generally unwise, but the impetus here was to get something the client could look at out the door as fast as possible, and as far as I know Python doesn't have that one-line command to invoke the Windows file selector that Blitz Plus has.