From 2541a0b8301a96ee649620bfeed1a6617d765262 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Wed, 6 Sep 2023 16:24:07 -0500 Subject: [PATCH 01/13] Removed Tree object and added code for drawing trees --- src/phylogeni.nim | 3 +- src/phylogeni/drawing.nim | 66 ++++++++++ src/phylogeni/io/parseNewick.nim | 88 +++++++------- src/phylogeni/io/writeNewick.nim | 14 +-- src/phylogeni/simulate.nim | 15 +-- src/phylogeni/tree.nim | 199 +++++++++---------------------- 6 files changed, 184 insertions(+), 201 deletions(-) create mode 100644 src/phylogeni/drawing.nim diff --git a/src/phylogeni.nim b/src/phylogeni.nim index fb026a4..6a25368 100644 --- a/src/phylogeni.nim +++ b/src/phylogeni.nim @@ -2,7 +2,8 @@ import ./phylogeni/[ tree, io/parseNewick, io/writeNewick, - simulate] + simulate + ] export tree, parseNewick, diff --git a/src/phylogeni/drawing.nim b/src/phylogeni/drawing.nim new file mode 100644 index 0000000..aa9aa68 --- /dev/null +++ b/src/phylogeni/drawing.nim @@ -0,0 +1,66 @@ +import ./tree + +import phylogeni + +type + DrawNode*[T] = ref object + x: float # Horizontal position of node, equivalent to node height + y: float # Vertical position of node + data: T + +template toClosure*(i): auto = + ## Wrap an inline iterator in a first-class closure iterator. + iterator j: type(i) {.closure.} = + for x in i: yield x + j + +proc copyToDrawNodeTree[T](tree: Node[T]): Node[DrawNode[T]] = + ## Copy tree structure and replace existing data with DrawNode type with + ## data being copied to the DrawNode data property + var copied = Node[DrawNode[T]](length:tree.length, label:tree.label, data:DrawNode[T](data:tree.data)) + for i in tree.children: + copied.addChild(copyToDrawNodeTree(i)) + result = copied + +proc getCoords*[T](tree: Node[T], branchLengthScaling=1.0, branchSepScaling=1.0): Node[DrawNode[T]] = + ## Return coordinates for a typical rectangular or slanted phylogeny + # TODO: Raise Error if branchLengthScaling or branchSepScaling is <=0 + var copied = copyToDrawNodeTree(tree) + + # Make newickorder a closure iterator using template + let newickOrderIt = toClosure(copied.newickorder) + + # Iter over nodes in newick order. Assign x on first pass of all nodes. + # Assign y when visiting leaves and second visit of each node. + var + root = newickOrderIt().node + leafY = 0.0 + root.data = DrawNode[T]() + root.data.x = root.length * branchSepScaling + for i in newickOrderIt(): + var n = i.node + if i.firstVisit: + # Assign x on first visit + n.data.x = n.parent.data.x + (n.length * branchLengthScaling) + # Assign y to leaves + if i.node.isLeaf: + n.data.y = leafY + leafY += branchSepScaling + else: + # Assign y on second visit of each internal node + if not n.isLeaf: + let + lo = n.children[0].data.y + up = n.children[^1].data.y + n.data.y = (up - lo) / 2 + lo + result = copied + +let t = parseNewickString("(B:1.0[Test],((E:1.0,F:1.0)D:1.0[Test],G:1.0)C:1.0)A:1.0;", typ=string) +let c = getCoords(t) +echo t.ascii +for i in c.preorder: + echo i.label, ", ", i.data.x, ", ", i.data.y +echo "" +let c2 = getCoords(t, branchLengthScaling=2.0, branchSepScaling=2.0) +for i in c2.preorder: + echo i.label, ", ", i.data.x, ", ", i.data.y \ No newline at end of file diff --git a/src/phylogeni/io/parseNewick.nim b/src/phylogeni/io/parseNewick.nim index ad014ff..f1555ac 100644 --- a/src/phylogeni/io/parseNewick.nim +++ b/src/phylogeni/io/parseNewick.nim @@ -3,6 +3,8 @@ # to occur anywhere which will be problematic if I make trees generic and # parseAnnotation mixins get called before the label and length is parsed. +# TODO: String annotation is not currently being parsed + import std/[streams, lexbase, strformat, strutils] import ../tree @@ -18,7 +20,7 @@ type # newickTopology, newickLabel, newickLength, newickEnd, newickEOF NewickParser*[T] = object of BaseLexer - tree: Tree[T] + root: Node[T] currNode*: Node[T] token: string state: NewickState @@ -182,7 +184,7 @@ proc parseTopology[T](p: var NewickParser[T]) = p.bufpos.inc() p.state = newickLabel of ';': - if p.currNode == p.tree.root: + if p.currNode == p.root: p.bufpos.inc() p.state = newickEnd else: @@ -205,9 +207,11 @@ proc parseStart[T](p: var NewickParser[T]) = if p.buf[p.bufpos+1] == '&': case p.buf[p.bufpos+2] of 'r', 'R': - p.tree.rooted = true + # p.tree.rooted = true + discard of 'u', 'U': - p.tree.rooted = false + # p.tree.rooted = false + discard else: p.bufpos.inc(2) p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") @@ -243,57 +247,57 @@ proc parseTree[T](p: var NewickParser[T]) = of newickEOF: break -proc parseNewickStream*[T](tree: var Tree[T], stream: Stream) = +proc parseNewickStream*(stream: Stream, typ: typedesc = void): Node[typ] = ## Parse a newick stream var - p = NewickParser[T]() - p.tree = tree - p.tree.root = Node[T]() - p.currNode = p.tree.root + p = NewickParser[typ]() + p.root = Node[typ]() + p.currNode = p.root p.open(stream) p.parseTree() p.close() + result = p.root -proc parseNewickStream*[T](treeSeq: var TreeSeq[T], stream: Stream) = - ## Parse a newick stream - var - p = NewickParser[T]() - p.open(stream) - while true: - p.state = newickStart - p.tree = Tree[T]() - p.tree.root = Node[T]() - p.currNode = p.tree.root - p.parseTree() - case p.state - of newickEOF: - break - of newickEnd: - treeSeq.add(p.tree) - else: - p.raiseError("Internal error, report possible bug") - p.close() +# proc parseNewickStream*[T](treeSeq: var TreeSeq[T], stream: Stream) = +# ## Parse a newick stream +# var +# p = NewickParser[T]() +# p.open(stream) +# while true: +# p.state = newickStart +# p.tree = Tree[T]() +# p.tree.root = Node[T]() +# p.currNode = p.tree.root +# p.parseTree() +# case p.state +# of newickEOF: +# break +# of newickEnd: +# treeSeq.add(p.tree) +# else: +# p.raiseError("Internal error, report possible bug") +# p.close() -proc parseNewickString*[T](tree: var Tree[T], str: string) = +proc parseNewickString*(str: string, typ: typedesc = void): Node[typ] = ## Parse a newick string var ss = newStringStream(str) - tree.parseNewickStream(ss) + result = parseNewickStream(ss, typ) ss.close() -proc parseNewickString*[T](treesSeq: var TreeSeq[T], str: string) = - ## Parse a newick string - var ss = newStringStream(str) - treesSeq.parseNewickStream(ss) - ss.close() +# proc parseNewickString*[T](treesSeq: var TreeSeq[T], str: string) = +# ## Parse a newick string +# var ss = newStringStream(str) +# treesSeq.parseNewickStream(ss) +# ss.close() -proc parseNewickFile*[T](tree: var Tree[T], path: string) = +proc parseNewickFile*(path: string, typ: typedesc = void): Node[typ] = ## Parse a newick file var fs = newFileStream(path, fmRead) - tree.parseNewickStream(fs) + result = parseNewickStream(fs, typ) fs.close() -proc parseNewickFile*[T](treeSeq: var TreeSeq[T], path: string) = - ## Parse a newick file - var fs = newFileStream(path, fmRead) - treeSeq.parseNewickStream(fs) - fs.close() \ No newline at end of file +# proc parseNewickFile*[T](treeSeq: var TreeSeq[T], path: string) = +# ## Parse a newick file +# var fs = newFileStream(path, fmRead) +# treeSeq.parseNewickStream(fs) +# fs.close() \ No newline at end of file diff --git a/src/phylogeni/io/writeNewick.nim b/src/phylogeni/io/writeNewick.nim index d8b11f0..e095dd7 100644 --- a/src/phylogeni/io/writeNewick.nim +++ b/src/phylogeni/io/writeNewick.nim @@ -13,13 +13,13 @@ func writeNewickData[T](node: Node[T], str: var string) = str.add(fmt":{$node.length}") node.writeAnnotation(str) -func writeNewickString*[T](tree: Tree[T]): string = +func writeNewickString*[T](tree: Node[T]): string = ## Write newick string for Node object var str = "" - if tree.rooted: - str.add("[&R]") - else: - str.add("[&U]") + # if tree.rooted: + # str.add("[&R]") + # else: + # str.add("[&U]") for i in tree.newickorder(): if i.firstVisit == true: if i.node.isLeaf(): @@ -31,12 +31,12 @@ func writeNewickString*[T](tree: Tree[T]): string = else: # is second visit to node str.add(")") i.node.writeNewickData(str) - if (i.node != tree.root) and (i.node != i.node.parent.children[^1]): # is not last node in parents children + if (i.node != tree) and (i.node != i.node.parent.children[^1]): # is not last node in parents children str.add(",") str.add(";") result = str -proc writeNewickFile*[T](tree: Tree[T], filename:string) = +proc writeNewickFile*[T](tree: Node[T], filename:string) = # Write a newick file for Node object var str = writeNewickString(tree) writeFile(filename, str) diff --git a/src/phylogeni/simulate.nim b/src/phylogeni/simulate.nim index ee4397b..5be4255 100644 --- a/src/phylogeni/simulate.nim +++ b/src/phylogeni/simulate.nim @@ -1,16 +1,17 @@ import std/[random, math] import ./tree +# TODO: Make BirthDeath Simulator Work # TODO: Make option to take random number generator object as an option proc randExp(l: float): float = -ln(rand(1.0))/l -proc uniformPureBirth*(nTips: int, birthRate: float=1.0, typ=void): Tree[typ] = +proc uniformPureBirth*(nTips: int, birthRate: float=1.0, typ=void): Node[typ] = ## Simulate tree under uniform pure birth process. var - t = Tree[typ](root: Node[typ]()) - leaves = @[t.root] + t = Node[typ]() + leaves = @[t] for i in 1 ..< nTips: var waitTime = randExp(float(leaves.len()) * birthRate) @@ -35,11 +36,11 @@ proc uniformPureBirth*(nTips: int, birthRate: float=1.0, typ=void): Tree[typ] = inc += 1 result = t -proc uniformBirthDeath*(nTips: int, birthRate=1.0, deathRate=1.0, rerun=false, typ=void): Tree[typ] = +proc uniformBirthDeath*(nTips: int, birthRate=1.0, deathRate=1.0, rerun=false, typ=void): Node[typ] = ## Simulate tree under uniform birth death process. var - t = Tree[typ](root: Node[typ]()) - leaves = @[t.root] + t = Node[typ]() + leaves = @[t] while true: if leaves.len() == nTips: break @@ -61,7 +62,7 @@ proc uniformBirthDeath*(nTips: int, birthRate=1.0, deathRate=1.0, rerun=false, t if leaves.len() == 1: # Rerun if rerun: - leaves.add(t.root) + leaves.add(t) # Or quit else: break diff --git a/src/phylogeni/tree.nim b/src/phylogeni/tree.nim index 6a2ae51..2ee7368 100644 --- a/src/phylogeni/tree.nim +++ b/src/phylogeni/tree.nim @@ -1,9 +1,3 @@ -# TODO: Should Tree have rooted: property and should functions like treeHeight -# and treeLength have behavior that is dependent on this property? -# Would make more sense if there was some way to enforce length of zero on the -# root node no matter what which is not done currently. -# Or should the Tree type be eliminated all together for simplicity. Is it useful? - import std/[algorithm, tables, hashes, strutils, sequtils] export algorithm.SortOrder @@ -16,31 +10,8 @@ type length*: float data*: T - Tree*[T] = ref object - root*: Node[T] - rooted*: bool - - TreeSeq*[T] = seq[Tree[T]] - TreeError* = object of CatchableError -func newTree*(typ: typedesc = void): Tree[typ] = Tree[typ]() - ## Create new Tree. - -func newNode*(label: string, length: float, typ: typedesc = void): Node[typ] = - ## Create new Node. - Node[typ](label:label, length:length) - -proc treeFromString*(str: string, typ: typedesc = void): Tree[typ] = - ## Read tree from string. - result = Tree[typ]() - result.parseNewickString(str) - -proc treeFromFile*(path: string, typ: typedesc = void): Tree[typ] = - ## Read tree from file. - result = Tree[typ]() - result.parseNewickFile(path) - func hash*[T](n: Node[T]): Hash = result = n.label.hash !& n.length.hash result = !$result @@ -62,27 +33,31 @@ func isLeaf*[T](node: Node[T]): bool = else: result = false -func prune*[T](tree: Tree[T], node: Node[T]) = - ## Prune node from tree. - var parent = node.parent - if node == tree.root: +func isRoot*[T](node: Node[T]): bool = + if node.parent == nil: + result = true + else: + result = false + +func prune*[T](tree, node: Node[T]) = + ## Prune branch leading to node from tree. + if node.parent == nil: raise newException(TreeError, "Cannot prune root node") + var parent = node.parent parent.children.delete(parent.children.find(node)) if parent.children.len() == 1: var child = parent.children[0] - child.length += parent.length - if parent == tree.root: - child.parent = nil - tree.root = child - else: - var grandparent = parent.parent - child.parent = grandparent - grandparent.children[grandparent.children.find(parent)] = child - -func prune*[T](tree: Tree[T], nodes: seq[Node[T]]) = - ## Prune nodes from tree. - for i in nodes: - tree.prune(i) + parent.length += child.length + parent.children = child.children + parent.label = child.label + +proc copyTree*[T](tree: Node[T], typ: typedesc = void): Node[typ] = + ## Copy the structure, edge lengths, and labels of a tree. The returned tree + ## may have a different data type. + var copied = Node[typ](length:tree.length, label:tree.label) + for i in tree.children: + copied.addChild(copyTree(i, typ)) + result = copied iterator preorder*[T](root: Node[T]): Node[T] = ## Preorder traverse. @@ -92,11 +67,6 @@ iterator preorder*[T](root: Node[T]): Node[T] = stack.add(node.children.reversed()) yield node -iterator preorder*[T](tree: Tree[T]): Node[T] = - ## Preorder traverse. - for i in tree.root.preorder(): - yield i - iterator postorder*[T](root: Node[T]): Node[T] = ## Postorder traverse. var @@ -110,11 +80,6 @@ iterator postorder*[T](root: Node[T]): Node[T] = var node = postStack.pop() yield node -iterator postorder*[T](tree: Tree[T]): Node[T] = - ## Postorder traverse. - for i in tree.root.postorder(): - yield i - iterator newickorder*[T](root: Node[T]): tuple[node:Node[T], firstVisit:bool] = ## Newick order traverse. All internal nodes are visited twice. var stack: seq[tuple[node: Node[T], firstVisit: bool]] @@ -132,11 +97,6 @@ iterator newickorder*[T](root: Node[T]): tuple[node:Node[T], firstVisit:bool] = else: stack.add((child, true)) -iterator newickorder*[T](tree: Tree[T]): tuple[node:Node[T], firstVisit: bool] = - ## Newick order traverse. All internal nodes are visited twice. - for i in tree.root.newickorder(): - yield i - iterator levelorder*[T](root: Node[T]): Node[T] = ## Levelorder traverse. yield root @@ -147,54 +107,15 @@ iterator levelorder*[T](root: Node[T]): Node[T] = yield node stack.add(node.children) -iterator levelorder*[T](tree: Tree[T]): Node[T] = - ## Levelorder traverse. - for i in tree.root.levelorder(): - yield i - -iterator inorder*[T](root: Node[T]): Node[T] = - ## Inorder traverse. Tree must be strictly bifurcating. - var - stack: seq[Node[T]] - current = root - while current != nil or stack.len > 0: - while current != nil: - stack.add(current) - if current.children.len == 2: - current = current.children[0] - elif current.children.len == 0: - current = nil - else: - raise newException(TreeError, "Tree must be strictly bifurcating for inorder traverse") - if stack.len > 0: - var node = stack.pop() - yield node - if node.children.len == 2: - current = node.children[1] - elif node.children.len == 0: - current = nil - else: - raise newException(TreeError, "Tree must be strictly bifurcating for inorder traverse") - -iterator inorder*[T](tree: Tree[T]): Node[T] = - ## Inorder traverse. Tree must be strictly bifurcating. - for i in tree.root.inorder(): - yield i - iterator iterleaves*[T](root: Node[T]): Node[T] = ## Iter over leaves. for i in root.preorder(): if i.is_leaf(): yield i -iterator iterleaves*[T](tree: Tree[T]): Node[T] = - ## Iter over leaves. - for i in tree.root.iterleaves(): - yield i - func ladderize*[T](root: Node[T], order: SortOrder = Ascending) = ## Ladderize subtree. - # TODO: Should reimplement with heap queue + # TODO: Should reimplement with heap queue and without using table var nodeDescendantCount = initTable[Node[T], int]() for node in root.postorder(): @@ -210,39 +131,48 @@ func ladderize*[T](root: Node[T], order: SortOrder = Ascending) = cmp=func(a, b: Node[T]): int = cmp(nodeDescendantCount[b], nodeDescendantCount[a]), order=order) -func ladderize*[T](tree: Tree[T], order: SortOrder = Ascending) = - ## Ladderize tree. - tree.root.ladderize(order) - -func calcTreeLength*[T](node: Node[T], includeRoot=true): float = +func calcTreeLength*[T](node: Node[T]): float = ## Calculate total length of tree. result = 0.0 - if includeRoot: - result += node.length for child in node.children: for i in child.preorder(): result += i.length -func calcTreeLength*[T](tree: Tree[T]): float = - ## Calculate total length of tree. - if tree.rooted: - tree.root.calcTreeLength(includeRoot=true) - else: - tree.root.calcTreeLength(includeRoot=false) - -func treeHeight*[T](node: Node[T], includeRoot=true): float = +func treeHeight*[T](node: Node[T]): float = ## Calculate the height of subtree. var maxHeight = 0.0 for child in node.children: let childHeight = treeHeight(child) maxHeight = max(maxHeight, childHeight) result = maxHeight + node.length - if not includeRoot: - result = result - node.length -func treeHeight*[T](tree: Tree[T], includeRoot=true): float = - ## Calculate the height of tree. - treeHeight(tree.root) +func findNode*[T](tree: Node[T], str: string): Node[T] = + ## Returns first instance of node label matching str. + for i in tree.preorder: + if i.label == str: + return i + +func getAncestors*[T](node: Node[T]): seq[Node[T]] = + var curr = node + while true: + if curr.parent != nil: + result.add(curr.parent) + curr = curr.parent + else: + break + +func getMRCA*[T](a, b: Node[T]): Node[T] = + ## Get the most recent common ancestor of two nodes. + # TODO: I think this could be faster adding the elements of the shoter list to a + # hash set and then checking if the elements of the other list belong to that set + let + aAncestors = a.getAncestors + bAncestors = b.getAncestors + for i in aAncestors: + for j in bAncestors: + if i == j: + return i + raise newException(TreeError, "No MRCA shared by nodes") func get_ascii[T](node: Node[T], char1="-", showInternal=true): tuple[clines: seq[string], mid:int] = ## Generates ascii string representation of tree. @@ -294,34 +224,15 @@ func get_ascii[T](node: Node[T], char1="-", showInternal=true): tuple[clines: se func ascii*[T](node: Node[T], char1="-", showInternal=true): string = ## Returns ascii string representation of tree. - var (lines, mid) = get_ascii(node, char1, showInternal) - result = lines.join("\n") - -func ascii*[T](tree: Tree[T], char1="-", showInternal=true): string = - ## Returns ascii string representation of tree. - var (lines, mid) = get_ascii(tree.root, char1, showInternal) + var (lines, _) = get_ascii(node, char1, showInternal) result = lines.join("\n") -func `$`*[T](tree: Tree[T]): string = - ## Returns ascii string representation of tree. - result = ascii(tree.root) - func `$`*[T](node: Node[T]): string = - ## Returns ascii string representation of tree. - result = ascii(node) + result = node.label # TODO: Implement these: -# func mrca*(tree: Tree, nodes: seq[Nodes]): Node = - ## Return node of most recent common ancestor - # func delete*(node: Node) = ## Remove only this node and not parent or children -# func extractTree*(node: Node): Tree = - ## Returns rooted tree - -# func calcTreeHeight*(node: Node): float = - ## Calculatate length from node or root of tree to furthest leaf - -# func findName*(name: string): Node = - +# func extractTreeCopy*[T](node: Node[T]): Node[T] = + # Return copy of tree rooted at node. \ No newline at end of file From d70b9369466be7a0e80db8a7c60a409047fce05d Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Tue, 19 Sep 2023 18:42:34 -0500 Subject: [PATCH 02/13] Simplification and code to compute coords for drawing --- src/phylogeni/drawing.nim | 14 +++++++++++++- src/phylogeni/tree.nim | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/phylogeni/drawing.nim b/src/phylogeni/drawing.nim index aa9aa68..33e2e06 100644 --- a/src/phylogeni/drawing.nim +++ b/src/phylogeni/drawing.nim @@ -1,3 +1,6 @@ +# Inherit from Node and only borrow the necessary procs +# Leave out or change some setters and getters + import ./tree import phylogeni @@ -8,6 +11,15 @@ type y: float # Vertical position of node data: T +# proc x*[T](n: DrawNode[T]): float = +# result = n.x + +# proc y*[T](n: DrawNode[T]): float = +# result = n.y + +# proc data*[T](n: DrawNode[T]): T = +# result = n.data + template toClosure*(i): auto = ## Wrap an inline iterator in a first-class closure iterator. iterator j: type(i) {.closure.} = @@ -63,4 +75,4 @@ for i in c.preorder: echo "" let c2 = getCoords(t, branchLengthScaling=2.0, branchSepScaling=2.0) for i in c2.preorder: - echo i.label, ", ", i.data.x, ", ", i.data.y \ No newline at end of file + echo i.label, ", ", i.data.x, ", ", i.data.y diff --git a/src/phylogeni/tree.nim b/src/phylogeni/tree.nim index 2ee7368..c9c8ea2 100644 --- a/src/phylogeni/tree.nim +++ b/src/phylogeni/tree.nim @@ -1,3 +1,6 @@ +#TODO: Make Node attributes private and make setters and getters +# or make Node a concept + import std/[algorithm, tables, hashes, strutils, sequtils] export algorithm.SortOrder From a17c85194a1d8a1e94f587cb099de945779eb26b Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Wed, 20 Sep 2023 11:29:03 -0500 Subject: [PATCH 03/13] Reimplemented everything as a concept --- src/phylogeni.nim | 127 +-- src/phylogeni/concepts.nim | 795 ++++++++++++++++++ src/phylogeni/coordinates.nim | 63 ++ src/phylogeni/drawing.nim | 78 -- src/phylogeni/io/writeNewick.nim | 42 - src/phylogeni/manipulate.nim | 56 ++ .../{io/parseNewick.nim => newickParser.nim} | 136 ++- src/phylogeni/newickWriter.nim | 30 + src/phylogeni/simulate.nim | 82 -- src/phylogeni/traverse.nim | 86 ++ src/phylogeni/tree.nim | 241 ------ src/test.nim | 51 ++ 12 files changed, 1151 insertions(+), 636 deletions(-) create mode 100644 src/phylogeni/concepts.nim create mode 100644 src/phylogeni/coordinates.nim delete mode 100644 src/phylogeni/drawing.nim delete mode 100644 src/phylogeni/io/writeNewick.nim create mode 100644 src/phylogeni/manipulate.nim rename src/phylogeni/{io/parseNewick.nim => newickParser.nim} (66%) create mode 100644 src/phylogeni/newickWriter.nim delete mode 100644 src/phylogeni/simulate.nim create mode 100644 src/phylogeni/traverse.nim delete mode 100644 src/phylogeni/tree.nim create mode 100644 src/test.nim diff --git a/src/phylogeni.nim b/src/phylogeni.nim index 6a25368..4e72ca1 100644 --- a/src/phylogeni.nim +++ b/src/phylogeni.nim @@ -1,114 +1,15 @@ import ./phylogeni/[ - tree, - io/parseNewick, - io/writeNewick, - simulate - ] - -export tree, - parseNewick, - writeNewick, - simulate - -## ========= -## PhylogeNi -## ========= -## -## PhylogeNi is a Nim library for working with phylogenetic trees. -## - -runnableExamples: - var t = treeFromString("(B:1.0,C:1.0)A:1.0;") - - echo t - - # -A /-B - # \-C - - for i in t.preorder(): - if i.label == "C": - i.addChild(newNode("D", 1.0)) - i.addChild(newNode("E", 1.0)) - t.ladderize(Ascending) - echo t - - # /C /-D - # -A| \-E - # \-B - - var str = t.writeNewickString() - echo str - # [&U]((D:1.0,E:1.0)C:1.0,B:1.0)A:1.0; - -## -## See the module docs for more details: -## `tree<./phylogeni/tree.html>`_ -## Provides basic functions for working with `Tree` and `Node` types such as: -## - Tree and Node creation -## - Topology modification -## - Tree iteration -## -## `parseNewick<./phylogeni/io/parseNewick.html>`_ -## Provides functions for reading trees from files or strings. -## -## `writeNewick<./phylogeni/io/writeNewick.html>`_ -## Provides functions for writing trees to files or strings. -## -## `simulate<./phylogeni/tree.html>`_ -## Provides functions for simulating trees: -## - Pure birth model -## - Birth death model -## -## Generic Node Data -## ================= -## `Node` is a generic type which can have any object stored in the data field. -## -## One great feature of PhylogeNi is that you do not need to completely rewrite your -## own parser/writer for custom data types when reading and writing a newick file or string. -## You only need to create `parseAnnotation` and `writeAnnotation` procs to handle -## reading or writing the annotation string. - -runnableExamples: - import std/strutils - import std/strformat - - type - CustomData = object - posterior: float - credibleInterval: tuple[lower, upper: float] - - let treeStr = "(B:1.0[&p:0.95,ci:0.9-1.0],C:1.0[&p:0.95,ci:0.9-1.0])A:1.0[&p:0.95,ci:0.9-1.0];" - - proc parseAnnotation(p: var NewickParser[CustomData], annotation: string) = - let annotations = annotation.split(",") - var dataCheck = (p: false, ci: false) - for i in annotations: - let split = i.split(":") - doAssert split.len == 2 - case split[0] - of "p": - p.currNode.data.posterior = parseFloat(split[1]) - dataCheck.p = true - of "ci": - let ci = split[1].split("-") - doAssert ci.len == 2 - p.currNode.data.credibleInterval = (parseFloat(ci[0]), parseFloat(ci[1])) - dataCheck.ci = true - else: - raise newException(NewickError, "Invalid Annotation") - if not dataCheck.p or not dataCheck.ci: - raise newException(NewickError, "") - - proc writeAnnotation(node: Node[CustomData], str: var string) = - str.add(fmt"[&p:{$node.data.posterior},ci:{$node.data.credibleInterval.lower}-{$node.data.credibleInterval.upper}]") - - let - t = treeFromString(treeStr, CustomData) - str = t.writeNewickString() - echo str - # [&U](B:1.0[&p:0.95,ci:0.9-1.0],C:1.0[&p:0.95,ci:0.9-1.0])A:1.0[&p:0.95,ci:0.9-1.0]; - - - - - + concepts, + coordinates, + manipulate, + newickParser, + newickWriter, + traverse] + +export + concepts, + coordinates, + manipulate, + newickParser, + newickWriter, + traverse \ No newline at end of file diff --git a/src/phylogeni/concepts.nim b/src/phylogeni/concepts.nim new file mode 100644 index 0000000..32e96ec --- /dev/null +++ b/src/phylogeni/concepts.nim @@ -0,0 +1,795 @@ +# import system +import std/[strutils, sequtils] + +type + TreeError* = object of CatchableError + +type + TraversableNode* = concept n, type T + n.parent is T + for i in n.children: + i is T + +func isLeaf*(node: TraversableNode): bool = + ## Check if node is leaf. + if node.children.len == 0: + result = true + else: + result = false + +func isRoot*(node: TraversableNode): bool = + if node.parent.isNil: + result = true + else: + result = false + +func getAncestors*(node: TraversableNode): seq[TraversableNode] = + var curr = node + while true: + if curr.parent != nil: + result.add(curr.parent) + curr = curr.parent + else: + break + +func getMRCA*(a, b: TraversableNode): TraversableNode = + ## Get the most recent common ancestor of two nodes. + # TODO: I think this could be faster adding the elements of the shorter list to a + # hash set and then checking if the elements of the other list belong to that set + let + aAncestors = a.getAncestors + bAncestors = b.getAncestors + for i in aAncestors: + for j in bAncestors: + if i == j: + return i + raise newException(TreeError, "No MRCA shared by nodes") + +############################### +# Labeled Node +type + LabeledNode* = concept n + n is TraversableNode + n.label is string + +func findNode*(tree: LabeledNode, str: string): LabeledNode = + ## Returns first instance of node label matching str. + for i in tree.preorder: + if i.label == str: + return i + +func `$`*(node: LabeledNode): string = + node.label + +func get_ascii(node: LabeledNode, char1="-", showInternal=true): tuple[clines: seq[string], mid:int] = + ## Generates ascii string representation of tree. + var + len = 3 + if node.children.len == 0 or showInternal == true: + if node.label.len > len: + len = node.label.len + var + pad = strutils.repeat(' ', len) + pa = strutils.repeat(' ', len-1) + if node.children.len > 0: + var + mids: seq[int] + results: seq[string] + for child in node.children: + var char2: string + if node.children.len == 1: + char2 = "-" + elif child == node.children[0]: + char2 = "/" + elif child == node.children[^1]: + char2 = "\\" + else: + char2 = "-" + var (clines, mid) = get_ascii(child, char2, showInternal) + mids.add(mid+len(results)) + results.add(clines) + var + lo = mids[0] + hi = mids[^1] + last = len(results) + mid = int((lo+hi)/2) + prefixes: seq[string] + prefixes.add(sequtils.repeat(pad, lo+1)) + if mids.len > 1: + prefixes.add(sequtils.repeat(pa & "|", hi-lo-1)) + prefixes.add(sequtils.repeat(pad, last-hi)) + prefixes[mid] = char1 & strutils.repeat("-", len-2) & prefixes[mid][^1] + var new_results: seq[string] + for (p, r) in zip(prefixes, results): + new_results.add(p&r) + if showInternal: + var stem = new_results[mid] + new_results[mid] = stem[0] & node.label & stem[node.label.len+1..^1] + result = (new_results, mid) + else: + result = (@[char1 & "-" & node.label], 0) + +func ascii*(node: LabeledNode, char1="-", showInternal=true): string = + ## Returns ascii string representation of tree. + var (lines, _) = get_ascii(node, char1, showInternal) + result = lines.join("\n") + + + +############################### +# Length Node +type + LengthNode* = concept n + n is TraversableNode + n.length is SomeNumber + +func calcTreeLength*(node: LengthNode): float = + ## Calculate total length of tree. + result = 0.0 + for child in node.children: + for i in child.preorder(): + result += i.length + +func treeHeight*(node: LengthNode): float = + ## Calculate the height of subtree. + var maxHeight = 0.0 + for child in node.children: + let childHeight = treeHeight(child) + maxHeight = max(maxHeight, childHeight) + result = maxHeight + node.length + + + +type + ReadableAnnotatedNode* = concept n + n is TraversableNode + n.parseAnnotation(string) + +type + WritableAnnotatedNode* = concept n + n is TraversableNode + n.writeAnnotation is string + + + + + + + + + + + + + + + + + + + + +# TODO: Delete everything below eventually, make sure everythnig was copied somewhere else +# import std/algorithm +# import std/strutils +# import std/sequtils +# import system + +# ############################################################# +# # Iterable Node +# type +# TraversableNode* = concept n, type T +# n.parent is T +# for i in n.children: +# i is T + +# # TODO: This causes an error, seems like a bug, reported https://github.com/nim-lang/Nim/issues/22723 +# # proc addChild(parent, child: TraversableNode) = +# # parent.children.add(child) +# # child.parent = parent + +# func isLeaf*(node: TraversableNode): bool = +# ## Check if node is leaf. +# if node.children.len == 0: +# result = true +# else: +# result = false + +# func isRoot*(node: TraversableNode): bool = +# if node.parent.isNil: +# result = true +# else: +# result = false + +# iterator preorder*(root: TraversableNode): TraversableNode = +# ## Preorder traverse. +# var stack = @[root] +# while stack.len > 0: +# var node = stack.pop() +# stack.add(node.children.reversed()) +# yield node + +# iterator postorder*(root: TraversableNode): TraversableNode = +# ## Postorder traverse. +# var +# preStack = @[root] +# postStack: seq[TraversableNode] +# while preStack.len > 0: +# var node = preStack.pop() +# postStack.add(node) +# preStack.add(node.children) +# while postStack.len > 0: +# var node = postStack.pop() +# yield node + +# iterator levelorder*(root: TraversableNode): TraversableNode = +# ## Levelorder traverse. +# yield root +# var stack = root.children +# while stack.len > 0: +# var node = stack[0] +# stack.delete(0) +# yield node +# stack.add(node.children) + +# iterator iterleaves*(root: TraversableNode): TraversableNode = +# ## Iter over leaves. +# for i in root.preorder(): +# if i.is_leaf(): +# yield i + +# # NewickOrder Iterator +# type +# NewickOrderState* = enum +# ascendingTree, descendingTree + +# NewickOrderNode*[T: TraversableNode] = ref object +# node*: T +# state*: NewickOrderState + +# func newNewickOrderNode[T](node: T, state: NewickOrderState): NewickOrderNode[T] = +# NewickOrderNode[T](node:node, state:state) + +# func children*[T](node: NewickOrderNode[T]): seq[T] = +# node.node.children + +# func parent*[T](node: NewickOrderNode[T]): T = +# node.node.parent + +# func isLeaf*[T](node: NewickOrderNode[T]): bool = +# ## Check if node is leaf. +# node.node.isLeaf + +# func isRoot*[T](node: NewickOrderNode[T]): bool = +# node.node.isRoot + +# proc `$`*[T](node: NewickOrderNode[T]): string = +# $node.node & ", " & $node.state + +# iterator newickorder*[T: TraversableNode](root: T): NewickOrderNode[T] = +# ## Newick order traverse. All internal nodes are visited twice. Leaf nodes are +# ## only visited once. This traverese is a hybrid between preorder and +# ## postorder traverse. It is convenient for writing newick strings and +# ## plotting trees. +# var stack: seq[NewickOrderNode[T]] +# stack.add(newNewickOrderNode(root, descendingTree)) +# stack.add(newNewickOrderNode(root, ascendingTree)) +# while stack.len > 0: +# var node = stack.pop() +# yield node +# if not node.isLeaf: +# if node.state == ascendingTree: +# for child in node.children.reversed: +# if not child.isLeaf: +# stack.add(newNewickOrderNode(child, descendingTree)) +# stack.add(newNewickOrderNode(child, ascendingTree)) +# else: +# stack.add(newNewickOrderNode(child, ascendingTree)) + + +# ################################################################ +# # Length Node +# type +# LengthNode = concept n +# n is TraversableNode +# n.length is SomeNumber + +# type +# ReadableAnnotatedNode = concept n +# n is TraversableNode +# n.parseAnnotation(string) + +# type +# WritableAnnotatedNode = concept n +# n is TraversableNode +# n.writeAnnotation is string + + + +# ################################################################ +# # Labelled Node +# type +# LabelledNode = concept n +# n is TraversableNode +# n.label is string + +# func `$`*(node: LabelledNode): string = +# node.label + +# func get_ascii(node: LabelledNode, char1="-", showInternal=true): tuple[clines: seq[string], mid:int] = +# ## Generates ascii string representation of tree. +# var +# len = 3 +# if node.children.len == 0 or showInternal == true: +# if node.label.len > len: +# len = node.label.len +# var +# pad = strutils.repeat(' ', len) +# pa = strutils.repeat(' ', len-1) +# if node.children.len > 0: +# var +# mids: seq[int] +# results: seq[string] +# for child in node.children: +# var char2: string +# if node.children.len == 1: +# char2 = "-" +# elif child == node.children[0]: +# char2 = "/" +# elif child == node.children[^1]: +# char2 = "\\" +# else: +# char2 = "-" +# var (clines, mid) = get_ascii(child, char2, showInternal) +# mids.add(mid+len(results)) +# results.add(clines) +# var +# lo = mids[0] +# hi = mids[^1] +# last = len(results) +# mid = int((lo+hi)/2) +# prefixes: seq[string] +# prefixes.add(sequtils.repeat(pad, lo+1)) +# if mids.len > 1: +# prefixes.add(sequtils.repeat(pa & "|", hi-lo-1)) +# prefixes.add(sequtils.repeat(pad, last-hi)) +# prefixes[mid] = char1 & strutils.repeat("-", len-2) & prefixes[mid][^1] +# var new_results: seq[string] +# for (p, r) in zip(prefixes, results): +# new_results.add(p&r) +# if showInternal: +# var stem = new_results[mid] +# new_results[mid] = stem[0] & node.label & stem[node.label.len+1..^1] +# result = (new_results, mid) +# else: +# result = (@[char1 & "-" & node.label], 0) + +# func ascii*(node: LabelledNode, char1="-", showInternal=true): string = +# ## Returns ascii string representation of tree. +# var (lines, _) = get_ascii(node, char1, showInternal) +# result = lines.join("\n") + + +# ##################################################### +# # Writing Newick String + +# func writeNewickData(node: TraversableNode, str: var string, annotation: bool) = +# when typeof(node) is LabelledNode: +# str.add(node.label) +# when typeof(node) is LengthNode: +# str.add(':') +# str.add($node.length) +# when typeof(node) is WritableAnnotatedNode: +# if annotation: +# str.add(node.writeAnnotation) + +# func writeNewickString*(root: TraversableNode, annotation=true): string = +# ## Write newick string for Node object +# var str = "" +# for i in root.newickorder(): +# if i.state == ascendingTree: +# if i.node.isLeaf(): +# i.node.writeNewickData(str, annotation) +# if i.node != i.parent.children[^1]: # not the first node in parents children +# str.add(",") +# else: # internal node +# str.add("(") +# else: # descending tree +# str.add(")") +# i.node.writeNewickData(str, annotation) +# if (i.node != root) and (i.node != i.parent.children[^1]): # not last node in parents children +# str.add(",") +# str.add(";") +# result = str + +# ################################################ +# ################################################ +# ################################################ +# ################################################ +# ################################################ +# ################################################ +# ################################################ +# ################################################ +# ################################################ +# ################################################ +# # Parse Newick + +# import std/[streams, lexbase, strformat, strutils] + +# type +# NewickError* = object of IOError + +# NewickState = enum +# newickStart, newickTopology, newickLabel, newickLength, newickAnnotation, +# newickEnd, newickEOF +# # TODO: This might be a better way to track state in order to raise errors if +# # a newick string doesn't have any parentheses. Low priority given how +# # unlikely that is. +# # newickStart, newickStartLabel, newickStartLength, newickStartTopology, +# # newickTopology, newickLabel, newickLength, newickEnd, newickEOF + +# NewickParser[T: TraversableNode] = object of BaseLexer +# root: T +# currNode: T +# token: string +# state: NewickState +# annotationState: bool # False if an annotation has already been parsed + +# const newickWhitespace = {' ', '\t', '\c', '\l'} + +# proc raiseError(p: NewickParser, msg: string) = +# var +# lineNum = $p.lineNumber +# colNum = $p.getColNumber(p.bufpos+1) +# m = fmt"{msg} at line {lineNum}, column {colNum}" +# raise newException(NewickError, m) + +# proc parseWhitespace(p: var NewickParser, skip=true) = +# while true: +# case p.buf[p.bufpos] +# of ' ', '\t': +# if not skip: p.token.add(p.buf[p.bufpos]) +# p.bufpos.inc() +# of '\c': +# if not skip: p.token.add(p.buf[p.bufpos]) +# p.bufpos = lexbase.handleCR(p, p.bufpos) +# of '\l': # same as \n +# if not skip: p.token.add(p.buf[p.bufpos]) +# p.bufpos = lexbase.handleLF(p, p.bufpos) +# else: +# break + +# # # proc parseAnnotation(p: var NewickParser[string], annotation: string) = +# # # p.currNode.data = annotation + +# # # proc parseAnnotation(p: var NewickParser[void], annotation: string) = +# # # discard + +# proc parseBracket(p: var NewickParser, showComments=false) = +# # TODO: handle unexpected end of file and newick statement +# mixin parseAnnotation +# p.token = "" +# p.bufpos.inc() +# while true: +# case p.buf[p.bufpos] +# of ']': +# p.bufpos.inc() +# break +# of newickWhitespace: +# p.parseWhitespace(skip=false) +# else: +# p.token.add(p.buf[p.bufpos]) +# p.bufpos.inc() +# if p.token.startswith('&'): +# if p.annotationState: +# # p.parseAnnotation(p.token[1..^1]) +# p.annotationState = false +# else: +# if showComments: +# echo p.token + +# proc parseLength(p: var NewickParser) = +# #TODO: Determine if length is float or int for nodetype and convert string appropriately +# var parseLength = true +# while true: +# case p.buf[p.bufpos] +# of '(', ',', ')', ';': +# p.state = newickTopology +# break +# of newickWhitespace: +# p.parseWhitespace() +# of '[': +# # p.parseBracket() +# p.state = newickAnnotation +# break +# of EndOfFile: +# p.raiseError("Unexpected end of stream") +# else: +# if parseLength: +# p.token = "" +# while true: +# case p.buf[p.bufpos] +# of '(', ',', ')', ';', '[', newickWhitespace, EndOfFile: +# parseLength = false +# break +# of '"': +# p.raiseError("Unexpected \"") +# else: +# p.token.add(p.buf[p.bufpos]) +# p.bufpos.inc() +# p.currNode.length = parseFloat(p.token) +# parseLength = false + +# proc parseLabel(p: var NewickParser) = +# # TODO: Write when statement to determine if node has label property +# var parseLabel = true +# p.annotationState = true +# while true: +# case p.buf[p.bufpos] +# of '(', ',', ')', ';': +# p.state = newickTopology +# break +# of ':': +# p.state = newickLength +# p.bufpos.inc() +# break +# of '[': +# p.state = newickAnnotation +# break +# # p.parseBracket() +# of newickWhitespace: +# p.parseWhitespace() +# of EndOfFile: +# p.raiseError("Unexpected end of stream") +# of '"': +# # Parse quoted text +# if parseLabel: +# p.token = "" +# p.bufpos.inc() +# while true: +# case p.buf[p.bufpos] +# of '"': +# p.bufpos.inc() +# break +# of newickWhitespace: +# p.parseWhitespace(skip=false) +# else: +# p.token.add(p.buf[p.bufpos]) +# p.bufpos.inc() +# p.currNode.label = p.token +# parseLabel = false +# else: +# p.raiseError("Unexpected \"") +# else: +# # Parse unquoted text +# if parseLabel: +# p.token = "" +# while true: +# case p.buf[p.bufpos] +# of '(', ',', ')', ';', ':', '[', ']', newickWhitespace, EndOfFile: +# parseLabel = false +# break +# of '"': +# p.raiseError("Unexpected \"") +# else: +# p.token.add(p.buf[p.bufpos]) +# p.bufpos.inc() +# p.currNode.label = p.token +# parseLabel = false +# else: +# p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") + +# proc parseData(p: var NewickParser) = +# var annotation = "" +# p.bufpos.inc +# while true: +# case p.buf[p.bufpos] +# of ']': +# p.state = newickTopology +# p.bufpos.inc() +# break +# else: +# annotation.add(p.buf[p.bufpos]) +# p.bufpos.inc() +# # TODO: Call annotation function if Node is annotabale +# when typeof(p.currNode) is ReadableAnnotatedNode: +# p.currNode.parseAnnotation(annotation) + +# proc parseTopology(p: var NewickParser, T: typedesc[TraversableNode]) = +# # Parse newick tree +# case p.buf[p.bufpos] +# of '(': +# var newNode = new(T) +# p.currNode.addChild(newNode) +# p.currNode = newNode +# p.bufpos.inc() +# p.state = newickLabel +# of ',': +# var newNode = new(T) +# p.currNode.parent.addChild(newNode) +# p.currNode = newNode +# p.bufpos.inc() +# p.state = newickLabel +# of ')': +# p.currNode = p.currNode.parent +# p.bufpos.inc() +# p.state = newickLabel +# of ';': +# if p.currNode == p.root: +# p.bufpos.inc() +# p.state = newickEnd +# else: +# p.raiseError("Mismatched parentheses") +# else: +# p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") + +# proc parseStart(p: var NewickParser) = +# # Parse beginning of newick file +# while true: +# case p.buf[p.bufpos] +# of '(': +# p.state = newickTopology +# break +# of newickWhitespace: +# p.parseWhitespace() +# of '[': +# if p.buf[p.bufpos+1] == '&': +# case p.buf[p.bufpos+2] +# of 'r', 'R': +# discard +# of 'u', 'U': +# discard +# else: +# p.bufpos.inc(2) +# p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") +# if p.buf[p.bufpos+3] == ']': +# p.bufpos.inc(4) +# else: +# p.bufpos.inc(3) +# p.raiseError("Expected \"]\"") +# else: +# p.parseBracket() +# of EndOfFile: +# # p.state = newickEOF +# # break +# p.raiseError("Unexpected end of file. No newick statment found.") +# else: +# p.state = newickLabel +# break + +# proc parseTree(p: var NewickParser, T: typedesc[TraversableNode]) = +# p.parseWhitespace() +# while true: +# case p.state +# of newickStart: +# p.parseStart() +# of newickTopology: +# p.parseTopology(T) +# of newickLabel: +# p.parseLabel() +# of newickLength: +# p.parseLength() +# of newickAnnotation: +# p.parseData() +# of newickEnd: +# break +# of newickEOF: +# break + +# proc parseNewickStream*(stream: Stream, T: typedesc[TraversableNode]): T = +# ## Parse a newick stream +# var +# p = NewickParser[T]() +# p.root = new(T) +# p.currNode = p.root +# p.open(stream) +# p.parseTree(T) +# p.close() +# result = p.root + +# proc parseNewickString*(str: string, T: typedesc[TraversableNode]): T = +# ## Parse a newick string +# var ss = newStringStream(str) +# result = parseNewickStream(ss, T) +# ss.close() + + +# ############################################# +# # Drawing + +# type +# CoordNode*[T] = ref object +# parent: CoordNode[T] +# children: seq[CoordNode[T]] +# x: float # Horizontal position of node, equivalent to node height +# y: float # Vertical position of node +# node: T + +# proc newCoordNode[T: TraversableNode](node: T): CoordNode[T] = +# result = CoordNode[T](node: new(T)) +# result.node[] = node[] + +# proc addChild[T: TraversableNode](parent, child: CoordNode[T]) = +# parent.children.add(child) +# child.parent = parent +# parent.node.children.add(child.node) +# child.node.parent = parent.node.parent +# # parent.node.addChild(child.node) # TODO: Use this when the proc for TraversableNode concept works + +# proc getCoords*[T: LengthNode](root: T, branchLengthScaling=1.0, branchSep=1.0): CoordNode[T] = +# ## Return coordinates for a typical rectangular or slanted phylogeny +# assert branchLengthScaling > 0 +# assert branchSep > 0 +# var +# leafY = 0.0 +# currNode = CoordNode[T](node: new(T)) # Placeholder, is parent to root node of new tree +# for i in root.newickorder: +# case i.state +# of ascendingTree: +# var newNode = newCoordNode(i.node) +# currNode.addChild(newNode) +# newNode.x = currNode.x + (i.node.length * branchLengthScaling) +# if i.node.isLeaf: +# newNode.y = leafY +# leafY += branchSep +# else: +# currNode = newNode +# of descendingTree: +# let +# lo = currNode.children[0].y +# up = currNode.children[^1].y +# currNode.y = (up - lo) / 2 + lo +# currNode = currNode.parent +# result = currNode.children[0] + + +# ############################################# +# # Testing + +# type +# Nd = ref object +# parent: Nd +# children: seq[Nd] +# label: string +# length: float +# data: string + +# proc addChild(parent, child: Nd) = +# # TODO: Make this a concept once that works +# parent.children.add(child) +# child.parent = parent + +# proc writeAnnotation(node: Nd): string = +# result.add('[') +# result.add(node.data) +# result.add(']') + +# proc parseAnnotation(node: Nd, str: string) = +# node.data = str + + + +# var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;", Nd) +# echo t.writeNewickString(false) + +# # Bad newick strings +# # TODO: Fix parser to catch these and raise exception with helpful error msg +# # var +# # str = "(B:1.0, [test]C:1.0)A:1.0;" #TODO: Fix error msg +# # str = "(B:1.0,C:[test]1.0)A:1.0;" #TODO: Fix error msg +# # str = "(B:1.0,C:1.0:[test])A:1.0;" #TODO: Fix error msg +# # str = "B:1.0,C:1.0:[test])A:1.0;" #TODO: Fix error msg +# # t = parseNewickString(str, Nd) + +# echo t.ascii + +# var c = t.getCoords() +# for i in c.preorder: +# echo i[] + + + + + + +# # diff --git a/src/phylogeni/coordinates.nim b/src/phylogeni/coordinates.nim new file mode 100644 index 0000000..09d1a6c --- /dev/null +++ b/src/phylogeni/coordinates.nim @@ -0,0 +1,63 @@ +import ./concepts, ./traverse + +type + CoordNode*[T] = ref object + parent: CoordNode[T] + children: seq[CoordNode[T]] + x: float # Horizontal position of node, equivalent to node height + y: float # Vertical position of node + node: T + +proc parent*[T](n: CoordNode[T]): CoordNode[T] = + n.parent + +proc children*[T](n: CoordNode[T]): seq[CoordNode[T]] = + n.children + +proc x*[T](n: CoordNode[T]): float = + n.x + +proc y*[T](n: CoordNode[T]): float = + n.x + +proc node*[T](n: CoordNode[T]): T = + n.node + +proc newCoordNode[T: TraversableNode](node: T): CoordNode[T] = + result = CoordNode[T](node: new(T)) + result.node[] = node[] + +proc addChild[T: TraversableNode](parent, child: CoordNode[T]) = + parent.children.add(child) + child.parent = parent + parent.node.children.add(child.node) + child.node.parent = parent.node.parent + # parent.node.addChild(child.node) # TODO: Use this when the proc for TraversableNode concept works + +proc getCoords*[T: LengthNode](root: T, branchLengthScaling=1.0, branchSep=1.0): CoordNode[T] = + ## Return coordinates for a typical rectangular or slanted phylogeny + assert branchLengthScaling > 0 + assert branchSep > 0 + var + leafY = 0.0 + currNode = CoordNode[T](node: new(T)) # Placeholder, is parent to root node of new tree + # TODO: Will this work?: + # currNode = CoordNode[T]() # Placeholder, is parent to root node of new tree + for i in root.newickorder: + case i.state + of ascendingTree: + var newNode = newCoordNode(i.node) + currNode.addChild(newNode) + newNode.x = currNode.x + (i.node.length * branchLengthScaling) + if i.node.isLeaf: + newNode.y = leafY + leafY += branchSep + else: + currNode = newNode + of descendingTree: + let + lo = currNode.children[0].y + up = currNode.children[^1].y + currNode.y = (up - lo) / 2 + lo + currNode = currNode.parent + result = currNode.children[0] diff --git a/src/phylogeni/drawing.nim b/src/phylogeni/drawing.nim deleted file mode 100644 index 33e2e06..0000000 --- a/src/phylogeni/drawing.nim +++ /dev/null @@ -1,78 +0,0 @@ -# Inherit from Node and only borrow the necessary procs -# Leave out or change some setters and getters - -import ./tree - -import phylogeni - -type - DrawNode*[T] = ref object - x: float # Horizontal position of node, equivalent to node height - y: float # Vertical position of node - data: T - -# proc x*[T](n: DrawNode[T]): float = -# result = n.x - -# proc y*[T](n: DrawNode[T]): float = -# result = n.y - -# proc data*[T](n: DrawNode[T]): T = -# result = n.data - -template toClosure*(i): auto = - ## Wrap an inline iterator in a first-class closure iterator. - iterator j: type(i) {.closure.} = - for x in i: yield x - j - -proc copyToDrawNodeTree[T](tree: Node[T]): Node[DrawNode[T]] = - ## Copy tree structure and replace existing data with DrawNode type with - ## data being copied to the DrawNode data property - var copied = Node[DrawNode[T]](length:tree.length, label:tree.label, data:DrawNode[T](data:tree.data)) - for i in tree.children: - copied.addChild(copyToDrawNodeTree(i)) - result = copied - -proc getCoords*[T](tree: Node[T], branchLengthScaling=1.0, branchSepScaling=1.0): Node[DrawNode[T]] = - ## Return coordinates for a typical rectangular or slanted phylogeny - # TODO: Raise Error if branchLengthScaling or branchSepScaling is <=0 - var copied = copyToDrawNodeTree(tree) - - # Make newickorder a closure iterator using template - let newickOrderIt = toClosure(copied.newickorder) - - # Iter over nodes in newick order. Assign x on first pass of all nodes. - # Assign y when visiting leaves and second visit of each node. - var - root = newickOrderIt().node - leafY = 0.0 - root.data = DrawNode[T]() - root.data.x = root.length * branchSepScaling - for i in newickOrderIt(): - var n = i.node - if i.firstVisit: - # Assign x on first visit - n.data.x = n.parent.data.x + (n.length * branchLengthScaling) - # Assign y to leaves - if i.node.isLeaf: - n.data.y = leafY - leafY += branchSepScaling - else: - # Assign y on second visit of each internal node - if not n.isLeaf: - let - lo = n.children[0].data.y - up = n.children[^1].data.y - n.data.y = (up - lo) / 2 + lo - result = copied - -let t = parseNewickString("(B:1.0[Test],((E:1.0,F:1.0)D:1.0[Test],G:1.0)C:1.0)A:1.0;", typ=string) -let c = getCoords(t) -echo t.ascii -for i in c.preorder: - echo i.label, ", ", i.data.x, ", ", i.data.y -echo "" -let c2 = getCoords(t, branchLengthScaling=2.0, branchSepScaling=2.0) -for i in c2.preorder: - echo i.label, ", ", i.data.x, ", ", i.data.y diff --git a/src/phylogeni/io/writeNewick.nim b/src/phylogeni/io/writeNewick.nim deleted file mode 100644 index e095dd7..0000000 --- a/src/phylogeni/io/writeNewick.nim +++ /dev/null @@ -1,42 +0,0 @@ -import ../tree -import std/strformat - -func writeAnnotation(node: Node[string], str: var string) = - str.add(fmt"[&{node.data}]") - -func writeAnnotation(node: Node[void], str: var string) = - discard - -func writeNewickData[T](node: Node[T], str: var string) = - mixin writeAnnotation - str.add(node.label) - str.add(fmt":{$node.length}") - node.writeAnnotation(str) - -func writeNewickString*[T](tree: Node[T]): string = - ## Write newick string for Node object - var str = "" - # if tree.rooted: - # str.add("[&R]") - # else: - # str.add("[&U]") - for i in tree.newickorder(): - if i.firstVisit == true: - if i.node.isLeaf(): - i.node.writeNewickData(str) - if i.node != i.node.parent.children[^1]: # not the first node in parents children - str.add(",") - else: # is internal node - str.add("(") - else: # is second visit to node - str.add(")") - i.node.writeNewickData(str) - if (i.node != tree) and (i.node != i.node.parent.children[^1]): # is not last node in parents children - str.add(",") - str.add(";") - result = str - -proc writeNewickFile*[T](tree: Node[T], filename:string) = - # Write a newick file for Node object - var str = writeNewickString(tree) - writeFile(filename, str) diff --git a/src/phylogeni/manipulate.nim b/src/phylogeni/manipulate.nim new file mode 100644 index 0000000..5e95cef --- /dev/null +++ b/src/phylogeni/manipulate.nim @@ -0,0 +1,56 @@ +import ./concepts, ./traverse +import std/algorithm + +export algorithm.SortOrder # Is this a bad practice? Is there an alternative? + +# proc addChild(parent, child: TraversableNode) = +# TODO: This causes an error, seems like a bug, reported https://github.com/nim-lang/Nim/issues/22723 +# parent.children.add(child) +# child.parent = parent + +func prune*(node: TraversableNode) = + ## Prune branch from its tree. + #TODO: Go through and fully delete descendant node so they can't be accessed later? + if node.parent == nil: + raise newException(TreeError, "Cannot prune root node") + var parent = node.parent + parent.children.delete(parent.children.find(node)) + if parent.children.len() == 1: + var child = parent.children[0] + parent.children = child.children + when node is LengthNode: + parent.length += child.length + when node is LabeledNode: + parent.label = child.label + +type + LadderNode[T] = ref object + parent: LadderNode[T] + children: seq[LadderNode[T]] + descendants: int + node: T + +proc ladderize*[T: TraversableNode](root: T, order: SortOrder = Ascending) = + ## Ladderize subtree. + # Should benchmark this against hash approach, first figure out implementing hashes with concept + # Could probably come up with more efficient way to sort using the current approach + # Getting the index of the sorted children rather than the children would be simpler + # and there wouldn't have to be a node attribute for LadderNode + var currNode = LadderNode[T]() + for i in root.newickorder: + case i.state + of ascendingTree: + var newNode = LadderNode[T](parent:currNode, node:i.node) + currNode.children.add(newNode) + if not i.node.isLeaf: + currNode = newNode + of descendingTree: + # Sort children of LadderNode + currNode.children.sort(cmp=func(a, b: LadderNode[T]): int = + cmp(a.descendants, b.descendants), order=order) + for ix, child in currNode.children: + # Reorder node children + currNode.node.children[ix] = currNode.children[ix].node + currNode.descendants += child.descendants + currNode.descendants += currNode.children.len + currNode = currNode.parent \ No newline at end of file diff --git a/src/phylogeni/io/parseNewick.nim b/src/phylogeni/newickParser.nim similarity index 66% rename from src/phylogeni/io/parseNewick.nim rename to src/phylogeni/newickParser.nim index f1555ac..7d6a727 100644 --- a/src/phylogeni/io/parseNewick.nim +++ b/src/phylogeni/newickParser.nim @@ -1,41 +1,35 @@ -# TODO: Should rewrite this a bit to be more constraining and to catch more errors -# before Nim does, such as when reading "A,B;":. Also regret allowing annotations -# to occur anywhere which will be problematic if I make trees generic and -# parseAnnotation mixins get called before the label and length is parsed. - -# TODO: String annotation is not currently being parsed - +import ./concepts, ./traverse import std/[streams, lexbase, strformat, strutils] -import ../tree type NewickError* = object of IOError NewickState = enum - newickStart, newickTopology, newickLabel, newickLength, newickEnd, newickEOF + newickStart, newickTopology, newickLabel, newickLength, newickAnnotation, + newickEnd, newickEOF # TODO: This might be a better way to track state in order to raise errors if # a newick string doesn't have any parentheses. Low priority given how # unlikely that is. # newickStart, newickStartLabel, newickStartLength, newickStartTopology, # newickTopology, newickLabel, newickLength, newickEnd, newickEOF - NewickParser*[T] = object of BaseLexer - root: Node[T] - currNode*: Node[T] + NewickParser[T: TraversableNode] = object of BaseLexer + root: T + currNode: T token: string state: NewickState annotationState: bool # False if an annotation has already been parsed const newickWhitespace = {' ', '\t', '\c', '\l'} -proc raiseError[T](p: NewickParser[T], msg: string) = +proc raiseError(p: NewickParser, msg: string) = var lineNum = $p.lineNumber colNum = $p.getColNumber(p.bufpos+1) m = fmt"{msg} at line {lineNum}, column {colNum}" raise newException(NewickError, m) -proc parseWhitespace[T](p: var NewickParser[T], skip=true) = +proc parseWhitespace(p: var NewickParser, skip=true) = while true: case p.buf[p.bufpos] of ' ', '\t': @@ -50,13 +44,13 @@ proc parseWhitespace[T](p: var NewickParser[T], skip=true) = else: break -proc parseAnnotation(p: var NewickParser[string], annotation: string) = - p.currNode.data = annotation +# # proc parseAnnotation(p: var NewickParser[string], annotation: string) = +# # p.currNode.data = annotation -proc parseAnnotation(p: var NewickParser[void], annotation: string) = - discard +# # proc parseAnnotation(p: var NewickParser[void], annotation: string) = +# # discard -proc parseBracket[T](p: var NewickParser[T], showComments=false) = +proc parseBracket(p: var NewickParser, showComments=false) = # TODO: handle unexpected end of file and newick statement mixin parseAnnotation p.token = "" @@ -73,13 +67,14 @@ proc parseBracket[T](p: var NewickParser[T], showComments=false) = p.bufpos.inc() if p.token.startswith('&'): if p.annotationState: - p.parseAnnotation(p.token[1..^1]) + # p.parseAnnotation(p.token[1..^1]) p.annotationState = false else: if showComments: echo p.token -proc parseLength[T](p: var NewickParser[T]) = +proc parseLength(p: var NewickParser) = + #TODO: Determine if length is float or int for nodetype and convert string appropriately var parseLength = true while true: case p.buf[p.bufpos] @@ -89,7 +84,9 @@ proc parseLength[T](p: var NewickParser[T]) = of newickWhitespace: p.parseWhitespace() of '[': - p.parseBracket() + # p.parseBracket() + p.state = newickAnnotation + break of EndOfFile: p.raiseError("Unexpected end of stream") else: @@ -108,7 +105,8 @@ proc parseLength[T](p: var NewickParser[T]) = p.currNode.length = parseFloat(p.token) parseLength = false -proc parseLabel[T](p: var NewickParser[T]) = +proc parseLabel(p: var NewickParser) = + # TODO: Write when statement to determine if node has label property var parseLabel = true p.annotationState = true while true: @@ -121,7 +119,9 @@ proc parseLabel[T](p: var NewickParser[T]) = p.bufpos.inc() break of '[': - p.parseBracket() + p.state = newickAnnotation + break + # p.parseBracket() of newickWhitespace: p.parseWhitespace() of EndOfFile: @@ -164,17 +164,33 @@ proc parseLabel[T](p: var NewickParser[T]) = else: p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") -proc parseTopology[T](p: var NewickParser[T]) = +proc parseData(p: var NewickParser) = + var annotation = "" + p.bufpos.inc + while true: + case p.buf[p.bufpos] + of ']': + p.state = newickTopology + p.bufpos.inc() + break + else: + annotation.add(p.buf[p.bufpos]) + p.bufpos.inc() + # TODO: Call annotation function if Node is annotabale + when typeof(p.currNode) is ReadableAnnotatedNode: + p.currNode.parseAnnotation(annotation) + +proc parseTopology(p: var NewickParser, T: typedesc[TraversableNode]) = # Parse newick tree case p.buf[p.bufpos] of '(': - var newNode = Node[T]() + var newNode = new(T) p.currNode.addChild(newNode) p.currNode = newNode p.bufpos.inc() p.state = newickLabel of ',': - var newNode = Node[T]() + var newNode = new(T) p.currNode.parent.addChild(newNode) p.currNode = newNode p.bufpos.inc() @@ -190,27 +206,23 @@ proc parseTopology[T](p: var NewickParser[T]) = else: p.raiseError("Mismatched parentheses") else: - p.raiseError(&"Internal error, report possible bug") + p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") -proc parseStart[T](p: var NewickParser[T]) = +proc parseStart(p: var NewickParser) = # Parse beginning of newick file while true: case p.buf[p.bufpos] of '(': p.state = newickTopology break - of ',': - p.raiseError("Unexpected comma. There can be only one root node.") of newickWhitespace: p.parseWhitespace() of '[': if p.buf[p.bufpos+1] == '&': case p.buf[p.bufpos+2] of 'r', 'R': - # p.tree.rooted = true discard of 'u', 'U': - # p.tree.rooted = false discard else: p.bufpos.inc(2) @@ -230,74 +242,38 @@ proc parseStart[T](p: var NewickParser[T]) = p.state = newickLabel break -proc parseTree[T](p: var NewickParser[T]) = +proc parseTree(p: var NewickParser, T: typedesc[TraversableNode]) = p.parseWhitespace() while true: case p.state of newickStart: p.parseStart() of newickTopology: - p.parseTopology() + p.parseTopology(T) of newickLabel: p.parseLabel() of newickLength: p.parseLength() + of newickAnnotation: + p.parseData() of newickEnd: break of newickEOF: break -proc parseNewickStream*(stream: Stream, typ: typedesc = void): Node[typ] = +proc parseNewickStream*(stream: Stream, T: typedesc[TraversableNode]): T = ## Parse a newick stream var - p = NewickParser[typ]() - p.root = Node[typ]() + p = NewickParser[T]() + p.root = new(T) p.currNode = p.root p.open(stream) - p.parseTree() + p.parseTree(T) p.close() result = p.root -# proc parseNewickStream*[T](treeSeq: var TreeSeq[T], stream: Stream) = -# ## Parse a newick stream -# var -# p = NewickParser[T]() -# p.open(stream) -# while true: -# p.state = newickStart -# p.tree = Tree[T]() -# p.tree.root = Node[T]() -# p.currNode = p.tree.root -# p.parseTree() -# case p.state -# of newickEOF: -# break -# of newickEnd: -# treeSeq.add(p.tree) -# else: -# p.raiseError("Internal error, report possible bug") -# p.close() - -proc parseNewickString*(str: string, typ: typedesc = void): Node[typ] = +proc parseNewickString*(str: string, T: typedesc[TraversableNode]): T = ## Parse a newick string var ss = newStringStream(str) - result = parseNewickStream(ss, typ) - ss.close() - -# proc parseNewickString*[T](treesSeq: var TreeSeq[T], str: string) = -# ## Parse a newick string -# var ss = newStringStream(str) -# treesSeq.parseNewickStream(ss) -# ss.close() - -proc parseNewickFile*(path: string, typ: typedesc = void): Node[typ] = - ## Parse a newick file - var fs = newFileStream(path, fmRead) - result = parseNewickStream(fs, typ) - fs.close() - -# proc parseNewickFile*[T](treeSeq: var TreeSeq[T], path: string) = -# ## Parse a newick file -# var fs = newFileStream(path, fmRead) -# treeSeq.parseNewickStream(fs) -# fs.close() \ No newline at end of file + result = parseNewickStream(ss, T) + ss.close() \ No newline at end of file diff --git a/src/phylogeni/newickWriter.nim b/src/phylogeni/newickWriter.nim new file mode 100644 index 0000000..74e43d0 --- /dev/null +++ b/src/phylogeni/newickWriter.nim @@ -0,0 +1,30 @@ +import ./concepts, ./traverse + +func writeNewickData(node: TraversableNode, str: var string, annotation: bool) = + when typeof(node) is LabeledNode: + str.add(node.label) + when typeof(node) is LengthNode: + str.add(':') + str.add($node.length) + when typeof(node) is WritableAnnotatedNode: + if annotation: + str.add(node.writeAnnotation) + +func writeNewickString*(root: TraversableNode, annotation=true): string = + ## Write newick string for Node object + var str = "" + for i in root.newickorder(): + if i.state == ascendingTree: + if i.node.isLeaf(): + i.node.writeNewickData(str, annotation) + if i.node != i.parent.children[^1]: # not the first node in parents children + str.add(",") + else: # internal node + str.add("(") + else: # descending tree + str.add(")") + i.node.writeNewickData(str, annotation) + if (i.node != root) and (i.node != i.parent.children[^1]): # not last node in parents children + str.add(",") + str.add(";") + result = str \ No newline at end of file diff --git a/src/phylogeni/simulate.nim b/src/phylogeni/simulate.nim deleted file mode 100644 index 5be4255..0000000 --- a/src/phylogeni/simulate.nim +++ /dev/null @@ -1,82 +0,0 @@ -import std/[random, math] -import ./tree - -# TODO: Make BirthDeath Simulator Work -# TODO: Make option to take random number generator object as an option - -proc randExp(l: float): float = - -ln(rand(1.0))/l - -proc uniformPureBirth*(nTips: int, birthRate: float=1.0, typ=void): Node[typ] = - ## Simulate tree under uniform pure birth process. - var - t = Node[typ]() - leaves = @[t] - for i in 1 ..< nTips: - var - waitTime = randExp(float(leaves.len()) * birthRate) - rLeaf = rand(leaves.len - 1) - # Add wait time to all leaves - for node in leaves: - node.length += waitTime - # Add descendant nodes to random leaf - for i in 0..1: - var nd = Node[typ]() - leaves[rLeaf].addChild(nd) - leaves.add(nd) - # Remove previous random leaf from leaf list since it is now internal node - leaves.delete(rLeaf) - # Add additional length and tip labels to final leaves - var - waitTime = randExp(float(leaves.len()) * birthRate) - inc = 1 - for node in leaves: - node.length += waitTime - node.label = "T" & $inc - inc += 1 - result = t - -proc uniformBirthDeath*(nTips: int, birthRate=1.0, deathRate=1.0, rerun=false, typ=void): Node[typ] = - ## Simulate tree under uniform birth death process. - var - t = Node[typ]() - leaves = @[t] - while true: - if leaves.len() == nTips: - break - var - waitTime = randExp(float(leaves.len()) * (birthRate + deathRate)) - rLeaf = rand(leaves.len - 1) - # Add wait time to all leaves - for node in leaves: - node.length += waitTime - # Determine if speciation or extinction even - if rand(1.0) < birthRate / (birthRate + deathRate): - # Speciation event - for i in 0..1: - var nd = Node[typ]() - leaves[rLeaf].addChild(nd) - leaves.add(nd) - else: - # Extinction event - if leaves.len() == 1: - # Rerun - if rerun: - leaves.add(t) - # Or quit - else: - break - else: - t.prune(leaves[rLeaf]) - # Delete random leaf from leaf list - leaves.delete(rLeaf) - # Add additional length and tip labels to final leaves - var - waitTime = randExp(float(leaves.len()) * birthRate) - inc = 1 - for node in leaves: - node.length += waitTime - node.label = "T" & $inc - inc += 1 - result = t - diff --git a/src/phylogeni/traverse.nim b/src/phylogeni/traverse.nim new file mode 100644 index 0000000..5e8241d --- /dev/null +++ b/src/phylogeni/traverse.nim @@ -0,0 +1,86 @@ +import ./concepts +import std/algorithm + +iterator preorder*(root: TraversableNode): TraversableNode = + ## Preorder traverse. + var stack = @[root] + while stack.len > 0: + var node = stack.pop() + stack.add(node.children.reversed()) + yield node + +iterator postorder*(root: TraversableNode): TraversableNode = + ## Postorder traverse. + var + preStack = @[root] + postStack: seq[TraversableNode] + while preStack.len > 0: + var node = preStack.pop() + postStack.add(node) + preStack.add(node.children) + while postStack.len > 0: + var node = postStack.pop() + yield node + +iterator levelorder*(root: TraversableNode): TraversableNode = + ## Levelorder traverse. + yield root + var stack = root.children + while stack.len > 0: + var node = stack[0] + stack.delete(0) + yield node + stack.add(node.children) + +iterator iterleaves*(root: TraversableNode): TraversableNode = + ## Iter over leaves. + for i in root.preorder(): + if i.is_leaf(): + yield i + +type + NewickOrderState* = enum + ascendingTree, descendingTree + + NewickOrderNode*[T: TraversableNode] = ref object + node*: T + state*: NewickOrderState + +func newNewickOrderNode[T](node: T, state: NewickOrderState): NewickOrderNode[T] = + NewickOrderNode[T](node:node, state:state) + +func children*[T](node: NewickOrderNode[T]): seq[T] = + node.node.children + +func parent*[T](node: NewickOrderNode[T]): T = + node.node.parent + +func isLeaf*[T](node: NewickOrderNode[T]): bool = + ## Check if node is leaf. + node.node.isLeaf + +func isRoot*[T](node: NewickOrderNode[T]): bool = + node.node.isRoot + +proc `$`*[T](node: NewickOrderNode[T]): string = + $node.node & ", " & $node.state + +iterator newickorder*[T: TraversableNode](root: T): NewickOrderNode[T] = + ## Newick order traverse. All internal nodes are visited twice. Leaf nodes are + ## only visited once. This traverese is a hybrid between preorder and + ## postorder traverse. It is convenient for writing newick strings and + ## plotting trees. + var stack: seq[NewickOrderNode[T]] + stack.add(newNewickOrderNode(root, descendingTree)) + stack.add(newNewickOrderNode(root, ascendingTree)) + while stack.len > 0: + var node = stack.pop() + yield node + if not node.isLeaf: + if node.state == ascendingTree: + for child in node.children.reversed: + if not child.isLeaf: + stack.add(newNewickOrderNode(child, descendingTree)) + stack.add(newNewickOrderNode(child, ascendingTree)) + else: + stack.add(newNewickOrderNode(child, ascendingTree)) \ No newline at end of file diff --git a/src/phylogeni/tree.nim b/src/phylogeni/tree.nim deleted file mode 100644 index c9c8ea2..0000000 --- a/src/phylogeni/tree.nim +++ /dev/null @@ -1,241 +0,0 @@ -#TODO: Make Node attributes private and make setters and getters -# or make Node a concept - -import std/[algorithm, tables, hashes, strutils, sequtils] - -export algorithm.SortOrder - -type - Node*[T] = ref object - parent*: Node[T] - children*: seq[Node[T]] - label*: string - length*: float - data*: T - - TreeError* = object of CatchableError - -func hash*[T](n: Node[T]): Hash = - result = n.label.hash !& n.length.hash - result = !$result - -func addChild*[T](parent: Node[T], newChild: Node[T]) = - ## Add child node to parent. - newChild.parent = parent - parent.children.add(newChild) - -func addSister*[T](node: Node[T], newSister: Node[T]) = - ## Add sister node. - newSister.parent = node.parent - node.parent.children.add(newSister) - -func isLeaf*[T](node: Node[T]): bool = - ## Check if node is leaf. - if node.children.len == 0: - result = true - else: - result = false - -func isRoot*[T](node: Node[T]): bool = - if node.parent == nil: - result = true - else: - result = false - -func prune*[T](tree, node: Node[T]) = - ## Prune branch leading to node from tree. - if node.parent == nil: - raise newException(TreeError, "Cannot prune root node") - var parent = node.parent - parent.children.delete(parent.children.find(node)) - if parent.children.len() == 1: - var child = parent.children[0] - parent.length += child.length - parent.children = child.children - parent.label = child.label - -proc copyTree*[T](tree: Node[T], typ: typedesc = void): Node[typ] = - ## Copy the structure, edge lengths, and labels of a tree. The returned tree - ## may have a different data type. - var copied = Node[typ](length:tree.length, label:tree.label) - for i in tree.children: - copied.addChild(copyTree(i, typ)) - result = copied - -iterator preorder*[T](root: Node[T]): Node[T] = - ## Preorder traverse. - var stack = @[root] - while stack.len > 0: - var node = stack.pop() - stack.add(node.children.reversed()) - yield node - -iterator postorder*[T](root: Node[T]): Node[T] = - ## Postorder traverse. - var - preStack = @[root] - postStack: seq[Node[T]] - while preStack.len > 0: - var node = preStack.pop() - postStack.add(node) - preStack.add(node.children) - while postStack.len > 0: - var node = postStack.pop() - yield node - -iterator newickorder*[T](root: Node[T]): tuple[node:Node[T], firstVisit:bool] = - ## Newick order traverse. All internal nodes are visited twice. - var stack: seq[tuple[node: Node[T], firstVisit: bool]] - stack.add((node: root, firstVisit: false)) - stack.add((node: root, firstVisit: true)) - while stack.len > 0: - var nodeTuple = stack.pop() - yield (nodeTuple) - if nodeTuple.node.children.len > 0: - if nodeTuple.firstVisit == true: - for child in nodeTuple.node.children.reversed: - if child.children.len > 0: - stack.add((child, false)) - stack.add((child, true)) - else: - stack.add((child, true)) - -iterator levelorder*[T](root: Node[T]): Node[T] = - ## Levelorder traverse. - yield root - var stack = root.children - while stack.len > 0: - var node = stack[0] - stack.delete(0) - yield node - stack.add(node.children) - -iterator iterleaves*[T](root: Node[T]): Node[T] = - ## Iter over leaves. - for i in root.preorder(): - if i.is_leaf(): - yield i - -func ladderize*[T](root: Node[T], order: SortOrder = Ascending) = - ## Ladderize subtree. - # TODO: Should reimplement with heap queue and without using table - var - nodeDescendantCount = initTable[Node[T], int]() - for node in root.postorder(): - if node.children.len == 0: - nodeDescendantCount[node] = 0 - else: - var total = 0 - for child in node.children: - total += nodeDescendantCount[child] - total += node.children.len - nodeDescendantCount[node] = total - node.children.sort( - cmp=func(a, b: Node[T]): int = cmp(nodeDescendantCount[b], - nodeDescendantCount[a]), order=order) - -func calcTreeLength*[T](node: Node[T]): float = - ## Calculate total length of tree. - result = 0.0 - for child in node.children: - for i in child.preorder(): - result += i.length - -func treeHeight*[T](node: Node[T]): float = - ## Calculate the height of subtree. - var maxHeight = 0.0 - for child in node.children: - let childHeight = treeHeight(child) - maxHeight = max(maxHeight, childHeight) - result = maxHeight + node.length - -func findNode*[T](tree: Node[T], str: string): Node[T] = - ## Returns first instance of node label matching str. - for i in tree.preorder: - if i.label == str: - return i - -func getAncestors*[T](node: Node[T]): seq[Node[T]] = - var curr = node - while true: - if curr.parent != nil: - result.add(curr.parent) - curr = curr.parent - else: - break - -func getMRCA*[T](a, b: Node[T]): Node[T] = - ## Get the most recent common ancestor of two nodes. - # TODO: I think this could be faster adding the elements of the shoter list to a - # hash set and then checking if the elements of the other list belong to that set - let - aAncestors = a.getAncestors - bAncestors = b.getAncestors - for i in aAncestors: - for j in bAncestors: - if i == j: - return i - raise newException(TreeError, "No MRCA shared by nodes") - -func get_ascii[T](node: Node[T], char1="-", showInternal=true): tuple[clines: seq[string], mid:int] = - ## Generates ascii string representation of tree. - var - len = 3 - if node.children.len == 0 or showInternal == true: - if node.label.len > len: - len = node.label.len - var - pad = strutils.repeat(' ', len) - pa = strutils.repeat(' ', len-1) - if node.children.len > 0: - var - mids: seq[int] - results: seq[string] - for child in node.children: - var char2: string - if node.children.len == 1: - char2 = "-" - elif child == node.children[0]: - char2 = "/" - elif child == node.children[^1]: - char2 = "\\" - else: - char2 = "-" - var (clines, mid) = get_ascii(child, char2, showInternal) - mids.add(mid+len(results)) - results.add(clines) - var - lo = mids[0] - hi = mids[^1] - last = len(results) - mid = int((lo+hi)/2) - prefixes: seq[string] - prefixes.add(sequtils.repeat(pad, lo+1)) - if mids.len > 1: - prefixes.add(sequtils.repeat(pa & "|", hi-lo-1)) - prefixes.add(sequtils.repeat(pad, last-hi)) - prefixes[mid] = char1 & strutils.repeat("-", len-2) & prefixes[mid][^1] - var new_results: seq[string] - for (p, r) in zip(prefixes, results): - new_results.add(p&r) - if showInternal: - var stem = new_results[mid] - new_results[mid] = stem[0] & node.label & stem[node.label.len+1..^1] - result = (new_results, mid) - else: - result = (@[char1 & "-" & node.label], 0) - -func ascii*[T](node: Node[T], char1="-", showInternal=true): string = - ## Returns ascii string representation of tree. - var (lines, _) = get_ascii(node, char1, showInternal) - result = lines.join("\n") - -func `$`*[T](node: Node[T]): string = - result = node.label - -# TODO: Implement these: -# func delete*(node: Node) = - ## Remove only this node and not parent or children - -# func extractTreeCopy*[T](node: Node[T]): Node[T] = - # Return copy of tree rooted at node. \ No newline at end of file diff --git a/src/test.nim b/src/test.nim new file mode 100644 index 0000000..26ba44b --- /dev/null +++ b/src/test.nim @@ -0,0 +1,51 @@ +import ./phylogeni + +type + Nd = ref object + parent: Nd + children: seq[Nd] + label: string + length: float + data: string + +proc addChild(parent, child: Nd) = + # TODO: Make this a concept once that works + parent.children.add(child) + child.parent = parent + +proc writeAnnotation(node: Nd): string = + result.add('[') + result.add(node.data) + result.add(']') + +proc parseAnnotation(node: Nd, str: string) = + node.data = str + +var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;", Nd) +echo t.writeNewickString(false) + +# Bad newick strings +# TODO: Fix parser to catch these and raise exception with helpful error msg +# var + # str = "(B:1.0, [test]C:1.0)A:1.0;" #TODO: Fix error msg + # str = "(B:1.0,C:[test]1.0)A:1.0;" #TODO: Fix error msg + # str = "(B:1.0,C:1.0:[test])A:1.0;" #TODO: Fix error msg + # str = "B:1.0,C:1.0:[test])A:1.0;" #TODO: Fix error msg + # t = parseNewickString(str, Nd) + +echo t.ascii +for i in t.preorder: + echo i.label +echo "" + +var c = t.getCoords() +for i in c.preorder(): + echo i[] +echo "" + +t.ladderize(Descending) +echo t.ascii +var c2 = t.getCoords() +for i in c2.preorder(): + echo i[] + From 196fa027a6faee31a8673ade9ce3666295949e55 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Thu, 21 Sep 2023 15:21:14 -0500 Subject: [PATCH 04/13] Wrote new parser, doesn't work with generics yet due to bug with npeg or nim --- src/phylogeni/manipulate.nim | 1 - src/phylogeni/newickParser.nim | 81 +++++++++++---------- src/phylogeni/newickParser2.nim | 120 +++++++++++++++++++++++++++++++ src/test.nim | 123 +++++++++++++++++++++----------- 4 files changed, 243 insertions(+), 82 deletions(-) create mode 100644 src/phylogeni/newickParser2.nim diff --git a/src/phylogeni/manipulate.nim b/src/phylogeni/manipulate.nim index 5e95cef..46e7676 100644 --- a/src/phylogeni/manipulate.nim +++ b/src/phylogeni/manipulate.nim @@ -10,7 +10,6 @@ export algorithm.SortOrder # Is this a bad practice? Is there an alternative? func prune*(node: TraversableNode) = ## Prune branch from its tree. - #TODO: Go through and fully delete descendant node so they can't be accessed later? if node.parent == nil: raise newException(TreeError, "Cannot prune root node") var parent = node.parent diff --git a/src/phylogeni/newickParser.nim b/src/phylogeni/newickParser.nim index 7d6a727..6705f95 100644 --- a/src/phylogeni/newickParser.nim +++ b/src/phylogeni/newickParser.nim @@ -1,3 +1,5 @@ +#TODO: Should rewrite this using a parser library, it has gotten too complex + import ./concepts, ./traverse import std/[streams, lexbase, strformat, strutils] @@ -50,7 +52,7 @@ proc parseWhitespace(p: var NewickParser, skip=true) = # # proc parseAnnotation(p: var NewickParser[void], annotation: string) = # # discard -proc parseBracket(p: var NewickParser, showComments=false) = +proc parseBracket(p: var NewickParser) = # TODO: handle unexpected end of file and newick statement mixin parseAnnotation p.token = "" @@ -69,11 +71,8 @@ proc parseBracket(p: var NewickParser, showComments=false) = if p.annotationState: # p.parseAnnotation(p.token[1..^1]) p.annotationState = false - else: - if showComments: - echo p.token -proc parseLength(p: var NewickParser) = +proc parseLength[T](p: var NewickParser[T]) = #TODO: Determine if length is float or int for nodetype and convert string appropriately var parseLength = true while true: @@ -121,7 +120,6 @@ proc parseLabel(p: var NewickParser) = of '[': p.state = newickAnnotation break - # p.parseBracket() of newickWhitespace: p.parseWhitespace() of EndOfFile: @@ -164,7 +162,12 @@ proc parseLabel(p: var NewickParser) = else: p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") -proc parseData(p: var NewickParser) = +# proc skipLabel(p: var NewickParser) = +# while true: +# case p.buf[p.bufpos] +# of + +proc parseData[T](p: var NewickParser[T]) = var annotation = "" p.bufpos.inc while true: @@ -180,7 +183,7 @@ proc parseData(p: var NewickParser) = when typeof(p.currNode) is ReadableAnnotatedNode: p.currNode.parseAnnotation(annotation) -proc parseTopology(p: var NewickParser, T: typedesc[TraversableNode]) = +proc parseTopology[T](p: var NewickParser[T]) = # Parse newick tree case p.buf[p.bufpos] of '(': @@ -218,22 +221,23 @@ proc parseStart(p: var NewickParser) = of newickWhitespace: p.parseWhitespace() of '[': - if p.buf[p.bufpos+1] == '&': - case p.buf[p.bufpos+2] - of 'r', 'R': - discard - of 'u', 'U': - discard - else: - p.bufpos.inc(2) - p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - if p.buf[p.bufpos+3] == ']': - p.bufpos.inc(4) - else: - p.bufpos.inc(3) - p.raiseError("Expected \"]\"") - else: - p.parseBracket() + p.parseBracket() + # if p.buf[p.bufpos+1] == '&': + # case p.buf[p.bufpos+2] + # of 'r', 'R': + # discard + # of 'u', 'U': + # discard + # else: + # p.bufpos.inc(2) + # p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") + # if p.buf[p.bufpos+3] == ']': + # p.bufpos.inc(4) + # else: + # p.bufpos.inc(3) + # p.raiseError("Expected \"]\"") + # else: + # p.parseBracket() of EndOfFile: # p.state = newickEOF # break @@ -242,16 +246,23 @@ proc parseStart(p: var NewickParser) = p.state = newickLabel break -proc parseTree(p: var NewickParser, T: typedesc[TraversableNode]) = - p.parseWhitespace() +proc parseNewickStream*(stream: Stream, T: typedesc[TraversableNode]): T = + ## Parse a newick stream + var p = NewickParser[T]() + p.root = new(T) + p.currNode = p.root + p.open(stream) while true: case p.state of newickStart: p.parseStart() of newickTopology: - p.parseTopology(T) + p.parseTopology() of newickLabel: + # when T is LabeledNode: p.parseLabel() + # when not T is LabeledNode: + # p.skipLabel() of newickLength: p.parseLength() of newickAnnotation: @@ -260,20 +271,14 @@ proc parseTree(p: var NewickParser, T: typedesc[TraversableNode]) = break of newickEOF: break - -proc parseNewickStream*(stream: Stream, T: typedesc[TraversableNode]): T = - ## Parse a newick stream - var - p = NewickParser[T]() - p.root = new(T) - p.currNode = p.root - p.open(stream) - p.parseTree(T) p.close() result = p.root -proc parseNewickString*(str: string, T: typedesc[TraversableNode]): T = +proc parseNewickString*(T: typedesc[TraversableNode], str: string): T = ## Parse a newick string var ss = newStringStream(str) result = parseNewickStream(ss, T) - ss.close() \ No newline at end of file + ss.close() + + + \ No newline at end of file diff --git a/src/phylogeni/newickParser2.nim b/src/phylogeni/newickParser2.nim new file mode 100644 index 0000000..1d06de6 --- /dev/null +++ b/src/phylogeni/newickParser2.nim @@ -0,0 +1,120 @@ + +type + Nd* = ref object + parent*: Nd + children*: seq[Nd] + label*: string + length*: float + data*: string + +proc addChild*(parent, child: Nd) = + parent.children.add(child) + child.parent = parent + +proc readNewickAnnotation*(n: Nd, data: string) = + n.data = data + +################################################################################ +# New parser + +import npeg +import ./concepts +import std/strutils, strformat + +type + NewickError* = object of IOError + +proc newChildNode(curr: var Nd) = + var newNode = Nd() + curr.addChild(newNode) + curr = newNode + +proc newSisterNode(curr: var Nd) = + var newNode = Nd() + curr.parent.addChild(newNode) + curr = newNode + +proc branchTerminated(curr: var Nd) = + curr = curr.parent + +proc parseLabel(curr: var Nd, label: string) = + when curr is LabeledNode: + curr.label = label + +proc parseLength(curr: var Nd, length: string) = + # TODO: Handle errors parsing int and float + when curr is LengthNode: + if length.len > 0: + when curr.length is int: + curr.length = parseInt(length) + when curr.length is float: + curr.length = parseFloat(length) + +proc parseData(curr: var Nd, data: string) = + when curr.is ReadableAnnotatedNode: + curr.parseNewickData(curr, data) + +proc parseNewickString(str:string): Nd = + var + root = new(Nd) + curr = root + let p = peg "newick": + # TODO: How to move this elsewhere or even simplify?: + NewickDataSymbols <- {' ', '!', '\"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\\', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'} + S <- *Space + comment <- ?(S * '[' * >*NewickDataSymbols * ']') + stop <- S * ';' + lBrack <- S * '(' * S: + newChildNode(curr) + rBrack <- S * ')' * S: + branchTerminated(curr) + comma <- S * ',' * S: + newSisterNode(curr) + label <- >+(Alpha | '_'): + parseLabel(curr, $1) + length <- ':' * S * >?(+Digit * ?('.' * +Digit)): + parseLength(curr, $1) + data <- '[' * >*NewickDataSymbols * ']': + parseData(curr, $1) + annotation <- ?label * S * ?length * S * ?data + leaf <- annotation + branchset <- (internal | leaf) * *(comma * (internal | leaf)) + internal <- lBrack * ?branchset * rBrack * annotation + newick <- comment * (internal | leaf) * stop * S * !1 + + let r = p.match(str) + if not r.ok: + var msg: string + if curr != root: + msg = "Invalid Newick string. May have unequal '(' and ')'" + else: + msg = &"Unexpected '{str[r.matchMax]}' at position {r.matchMax} of Newick string. Problem may originate before this position." + raise newException(NewickError, msg) + result = root + +proc parseNewickFile(path: string): Nd = + var str = readFile(path) + result = parseNewickString(str) + +################################################### +# Testing + +var + str = "(A,B:,(C,D));" + t = parseNewickString(str) +echo t.ascii + +# import ./traverse +# for i in t.preorder: + # echo i[] + +# discard parseNewickString("(,,(,));") +# discard parseNewickString("(A,B,(C,D));") +# discard parseNewickString("(A,B,(C,D)E)F;") +# discard parseNewickString("(:0.1,:0.2,(:0.3,:0.4):0.5);") +# discard parseNewickString("(:0.1,:0.2,(:0.3,:0.4):0.5):0.0;") +# discard parseNewickString("(A:0.1,B:0.2,(C:0.3,D:0.4):0.5);") +# discard parseNewickString("(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F;") +# discard parseNewickString("((B:0.2,(C:0.3,D:0.4)E:0.5)F:0.1)A;") +# # TODO: Make test cases with data annotation +# # TODO: Make test cases expected to fail \ No newline at end of file diff --git a/src/test.nim b/src/test.nim index 26ba44b..a896c4c 100644 --- a/src/test.nim +++ b/src/test.nim @@ -1,51 +1,88 @@ import ./phylogeni type - Nd = ref object - parent: Nd - children: seq[Nd] - label: string - length: float - data: string - -proc addChild(parent, child: Nd) = + Nd* = ref object + parent*: Nd + children*: seq[Nd] + label*: string + length*: float + +proc addChild*(parent, child: Nd) = + ## A bug in Nim currently requires that each type matching that is + ## a TraversableNode must have an addChild proc written for it. + ## This will no longer be necesary when the bug is fixed + ## https://github.com/nim-lang/Nim/issues/22723 # TODO: Make this a concept once that works parent.children.add(child) child.parent = parent -proc writeAnnotation(node: Nd): string = - result.add('[') - result.add(node.data) - result.add(']') - -proc parseAnnotation(node: Nd, str: string) = - node.data = str - -var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;", Nd) -echo t.writeNewickString(false) - -# Bad newick strings -# TODO: Fix parser to catch these and raise exception with helpful error msg -# var - # str = "(B:1.0, [test]C:1.0)A:1.0;" #TODO: Fix error msg - # str = "(B:1.0,C:[test]1.0)A:1.0;" #TODO: Fix error msg - # str = "(B:1.0,C:1.0:[test])A:1.0;" #TODO: Fix error msg - # str = "B:1.0,C:1.0:[test])A:1.0;" #TODO: Fix error msg - # t = parseNewickString(str, Nd) - -echo t.ascii -for i in t.preorder: - echo i.label -echo "" - -var c = t.getCoords() -for i in c.preorder(): - echo i[] -echo "" - -t.ladderize(Descending) -echo t.ascii -var c2 = t.getCoords() -for i in c2.preorder(): - echo i[] +# var t = parseNewickString(Nd, "(((f:1.0,g:1.0)e:1.0,d:1.0)c:1.0,b:1.0)a:1.0;") +# echo t.ascii +# for i in t.preorder: +# echo i.label +# t.ladderize() +# echo t.ascii +# prune(t.findNode("f")) +# echo t.ascii +# echo t.writeNewickString() + +# # TODO: Write tests to ensure all of these fail. +# discard parseNewickString(Nd, "(B:1.0, [test]C:1.0)A:1.0;") +# discard parseNewickString(Nd, "(B:1.0,C:[test]1.0)A:1.0;") +# discard parseNewickString(Nd, "(B:1.0,C:1.0:[test])A:1.0;") # This is not caught as an exception +# discard parseNewickString(Nd, "B:1.0,C:1.0:[test])A:1.0;") # This is not caught as an exception +# discard parseNewickString(Nd, "B:1.0[test]") # This is not caught as an exception + +# echo parseNewickString(Nd, "B:1.0[test]").ascii + + + +# type +# Nd*[T] = ref object +# parent*: Nd[T] +# children*: seq[Nd[T]] +# label*: string +# length*: float +# data*: T + +# proc addChild*[T](parent, child: Nd[t]) = +# # TODO: Make this a concept once that works +# parent.children.add(child) +# child.parent = parent + +# proc writeAnnotation*[void](node: Nd[T]): string = +# result.add('[') +# result.add(node.data) +# result.add(']') + +# proc parseAnnotation(node: Nd, str: string) = +# node.data = str + +# var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;", Nd) +# echo t.writeNewickString(false) + +# # Bad newick strings +# # TODO: Fix parser to catch these and raise exception with helpful error msg +# # var +# # str = "(B:1.0, [test]C:1.0)A:1.0;" +# # str = "(B:1.0,C:[test]1.0)A:1.0;" +# # str = "(B:1.0,C:1.0:[test])A:1.0;" +# # str = "B:1.0,C:1.0:[test])A:1.0;" +# # t = parseNewickString(str, Nd) + +# echo t.ascii +# for i in t.preorder: +# echo i.label +# echo "" + +# var c = t.getCoords() +# for i in c.preorder(): +# echo i[] +# echo "" + +# t.ladderize(Descending) +# echo t.ascii +# var c2 = t.getCoords() +# for i in c2.preorder(): +# echo i[] From 4db6e6fcbae05291ac180dc7b4a2575c69b8bc84 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Sat, 23 Sep 2023 11:34:20 -0500 Subject: [PATCH 05/13] Working on reimplementation with concepts and rewriting parser with Npeg --- src/phylogeni.nim | 4 +- src/phylogeni/concepts.nim | 10 +-- src/phylogeni/coordinates.nim | 4 +- src/phylogeni/newickParser2.nim | 108 +++++++++++++++++++------------- src/phylogeni/newickWriter.nim | 17 ++--- src/test.nim | 70 +++++++++------------ 6 files changed, 114 insertions(+), 99 deletions(-) diff --git a/src/phylogeni.nim b/src/phylogeni.nim index 4e72ca1..0f65088 100644 --- a/src/phylogeni.nim +++ b/src/phylogeni.nim @@ -2,7 +2,7 @@ import ./phylogeni/[ concepts, coordinates, manipulate, - newickParser, + newickParser2, newickWriter, traverse] @@ -10,6 +10,6 @@ export concepts, coordinates, manipulate, - newickParser, + newickParser2, newickWriter, traverse \ No newline at end of file diff --git a/src/phylogeni/concepts.nim b/src/phylogeni/concepts.nim index 32e96ec..7f0228b 100644 --- a/src/phylogeni/concepts.nim +++ b/src/phylogeni/concepts.nim @@ -140,15 +140,17 @@ func treeHeight*(node: LengthNode): float = +############################### +# Data Node type - ReadableAnnotatedNode* = concept n + ReadableDataNode* = concept n n is TraversableNode - n.parseAnnotation(string) + n.parseNewickData(string) type - WritableAnnotatedNode* = concept n + WritableDataNode* = concept n n is TraversableNode - n.writeAnnotation is string + n.writeNewickData is string diff --git a/src/phylogeni/coordinates.nim b/src/phylogeni/coordinates.nim index 09d1a6c..27c7d28 100644 --- a/src/phylogeni/coordinates.nim +++ b/src/phylogeni/coordinates.nim @@ -41,8 +41,6 @@ proc getCoords*[T: LengthNode](root: T, branchLengthScaling=1.0, branchSep=1.0): var leafY = 0.0 currNode = CoordNode[T](node: new(T)) # Placeholder, is parent to root node of new tree - # TODO: Will this work?: - # currNode = CoordNode[T]() # Placeholder, is parent to root node of new tree for i in root.newickorder: case i.state of ascendingTree: @@ -60,4 +58,4 @@ proc getCoords*[T: LengthNode](root: T, branchLengthScaling=1.0, branchSep=1.0): up = currNode.children[^1].y currNode.y = (up - lo) / 2 + lo currNode = currNode.parent - result = currNode.children[0] + result = currNode.children[0] \ No newline at end of file diff --git a/src/phylogeni/newickParser2.nim b/src/phylogeni/newickParser2.nim index 1d06de6..8c29576 100644 --- a/src/phylogeni/newickParser2.nim +++ b/src/phylogeni/newickParser2.nim @@ -1,4 +1,5 @@ +# TODO: Remove this once generics are fixed type Nd* = ref object parent*: Nd @@ -11,36 +12,47 @@ proc addChild*(parent, child: Nd) = parent.children.add(child) child.parent = parent -proc readNewickAnnotation*(n: Nd, data: string) = +proc parseNewickData*(n: Nd, data: string) = n.data = data + + ################################################################################ # New parser +#TODO: Make parser accept generics once bug is fixed +# https://github.com/zevv/npeg/issues/68 +# https://github.com/nim-lang/Nim/issues/22740 + import npeg import ./concepts -import std/strutils, strformat +import std/[strutils, strformat] type NewickError* = object of IOError +# proc newChildNode[T](curr: var T) = proc newChildNode(curr: var Nd) = var newNode = Nd() curr.addChild(newNode) curr = newNode +# proc newSisterNode[T](curr: var T) = proc newSisterNode(curr: var Nd) = var newNode = Nd() curr.parent.addChild(newNode) curr = newNode +# proc branchTerminated[T](curr: var T) = proc branchTerminated(curr: var Nd) = curr = curr.parent +# proc parseLabel[T](curr: var T, label: string) = proc parseLabel(curr: var Nd, label: string) = when curr is LabeledNode: curr.label = label +# proc parseLength[T](curr: var T, length: string) = proc parseLength(curr: var Nd, length: string) = # TODO: Handle errors parsing int and float when curr is LengthNode: @@ -50,63 +62,75 @@ proc parseLength(curr: var Nd, length: string) = when curr.length is float: curr.length = parseFloat(length) +# proc parseData[T](curr: var T, data: string) = proc parseData(curr: var Nd, data: string) = - when curr.is ReadableAnnotatedNode: - curr.parseNewickData(curr, data) + when curr.is ReadableDataNode: + # mixin parseNewickData + parseNewickData(curr, data) -proc parseNewickString(str:string): Nd = +# proc parseNewickString*(T: typedesc[TraversableNode], str:string): T = +proc parseNewickString*(str:string): Nd = var + # root = new(T) root = new(Nd) curr = root + dataState = true let p = peg "newick": # TODO: How to move this elsewhere or even simplify?: - NewickDataSymbols <- {' ', '!', '\"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\\', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'} - S <- *Space - comment <- ?(S * '[' * >*NewickDataSymbols * ']') - stop <- S * ';' - lBrack <- S * '(' * S: - newChildNode(curr) - rBrack <- S * ')' * S: - branchTerminated(curr) - comma <- S * ',' * S: - newSisterNode(curr) - label <- >+(Alpha | '_'): - parseLabel(curr, $1) - length <- ':' * S * >?(+Digit * ?('.' * +Digit)): - parseLength(curr, $1) - data <- '[' * >*NewickDataSymbols * ']': - parseData(curr, $1) - annotation <- ?label * S * ?length * S * ?data + dataChars <- Print - {'[', ']'} + S <- *Space + comment <- ?('[' * >*dataChars * ']') + # TODO: Why doesn't this work? + # nestComment <- >('[' * *(dataChars | nestComment ) * ']') + # comment <- ?('[' * >*(dataChars | nested) * ']') + stop <- ';' + lBrack <- '(' : + newChildNode(curr) + rBrack <- ')' : + branchTerminated(curr) + comma <- ',' : + newSisterNode(curr) + label <- >+(Alnum | '_'): + parseLabel(curr, $1) + length <- ':' * >?(+Digit * ?('.' * +Digit)): + parseLength(curr, $1) + data <- '[' * >*dataChars * ']': + parseData(curr, $1) + annotation <- ?data * S * ?label * S * ?data * S * ?length * S * ?data: + dataState=true leaf <- annotation - branchset <- (internal | leaf) * *(comma * (internal | leaf)) - internal <- lBrack * ?branchset * rBrack * annotation - newick <- comment * (internal | leaf) * stop * S * !1 + branchset <- (internal | leaf) * S * *(comma * S * (internal | leaf)) + internal <- S * lBrack * S * ?branchset * S * rBrack * S * annotation + newick <- S * comment * (internal | leaf) * S * stop * S * !1 let r = p.match(str) + echo r if not r.ok: - var msg: string - if curr != root: - msg = "Invalid Newick string. May have unequal '(' and ')'" - else: - msg = &"Unexpected '{str[r.matchMax]}' at position {r.matchMax} of Newick string. Problem may originate before this position." + var msg = &"Unexpected '{str[r.matchMax]}' at position {r.matchMax} of Newick string. Problem may originate before this position." + raise newException(NewickError, msg) + if curr != root: + var msg = "Invalid Newick string." raise newException(NewickError, msg) result = root -proc parseNewickFile(path: string): Nd = +# proc parseNewickFile*(T: typedesc[TraversableNode], path: string): T = +proc parseNewickFile*(path: string): Nd = var str = readFile(path) result = parseNewickString(str) -################################################### -# Testing -var - str = "(A,B:,(C,D));" - t = parseNewickString(str) -echo t.ascii -# import ./traverse -# for i in t.preorder: - # echo i[] +# ################################################### +# # Testing + +# var +# str = "(A:1.0[Test],B,(C,D));" +# t = parseNewickString(str) +# echo t.ascii + +# # import ./traverse +# # for i in t.preorder: +# # echo i[] # discard parseNewickString("(,,(,));") # discard parseNewickString("(A,B,(C,D));") @@ -116,5 +140,5 @@ echo t.ascii # discard parseNewickString("(A:0.1,B:0.2,(C:0.3,D:0.4):0.5);") # discard parseNewickString("(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F;") # discard parseNewickString("((B:0.2,(C:0.3,D:0.4)E:0.5)F:0.1)A;") -# # TODO: Make test cases with data annotation -# # TODO: Make test cases expected to fail \ No newline at end of file +# # # TODO: Make test cases with data annotation +# # # TODO: Make test cases expected to fail \ No newline at end of file diff --git a/src/phylogeni/newickWriter.nim b/src/phylogeni/newickWriter.nim index 74e43d0..b96041e 100644 --- a/src/phylogeni/newickWriter.nim +++ b/src/phylogeni/newickWriter.nim @@ -1,29 +1,32 @@ import ./concepts, ./traverse -func writeNewickData(node: TraversableNode, str: var string, annotation: bool) = +func writeAnnotations(node: TraversableNode, str: var string, data: bool) = when typeof(node) is LabeledNode: str.add(node.label) when typeof(node) is LengthNode: str.add(':') str.add($node.length) - when typeof(node) is WritableAnnotatedNode: - if annotation: - str.add(node.writeAnnotation) + when typeof(node) is WritableDataNode: + mixin writeNewickData + if data: + str.add('[') + str.add(node.writeNewickData) + str.add(']') -func writeNewickString*(root: TraversableNode, annotation=true): string = +func writeNewickString*(root: TraversableNode, data=true): string = ## Write newick string for Node object var str = "" for i in root.newickorder(): if i.state == ascendingTree: if i.node.isLeaf(): - i.node.writeNewickData(str, annotation) + i.node.writeAnnotations(str, data) if i.node != i.parent.children[^1]: # not the first node in parents children str.add(",") else: # internal node str.add("(") else: # descending tree str.add(")") - i.node.writeNewickData(str, annotation) + i.node.writeAnnotations(str, data) if (i.node != root) and (i.node != i.parent.children[^1]): # not last node in parents children str.add(",") str.add(";") diff --git a/src/test.nim b/src/test.nim index a896c4c..db5ac8c 100644 --- a/src/test.nim +++ b/src/test.nim @@ -1,40 +1,38 @@ import ./phylogeni -type - Nd* = ref object - parent*: Nd - children*: seq[Nd] - label*: string - length*: float - -proc addChild*(parent, child: Nd) = - ## A bug in Nim currently requires that each type matching that is - ## a TraversableNode must have an addChild proc written for it. - ## This will no longer be necesary when the bug is fixed - ## https://github.com/nim-lang/Nim/issues/22723 - # TODO: Make this a concept once that works - parent.children.add(child) - child.parent = parent - -# var t = parseNewickString(Nd, "(((f:1.0,g:1.0)e:1.0,d:1.0)c:1.0,b:1.0)a:1.0;") -# echo t.ascii +# TODO: This code is currently copied to newickParser2 until bug with parser is resolved +# type +# Nd* = ref object +# parent*: Nd +# children*: seq[Nd] +# label*: string +# length*: float + +# proc addChild*(parent, child: Nd) = +# ## A bug in Nim currently requires that each type matching that is +# ## a TraversableNode must have an addChild proc written for it. +# ## This will no longer be necesary when the bug is fixed +# ## https://github.com/nim-lang/Nim/issues/22723 +# # TODO: Make this a concept once that works +# parent.children.add(child) +# child.parent = parent + +# proc parseNewickData*(n: Nd, data: string) = +# n.data = data + +proc writeNewickData*(n: Nd): string = + n.data + +var t = parseNewickString("((([]f:1.0[Test],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") +echo t.ascii # for i in t.preorder: -# echo i.label +# echo i[] # t.ladderize() # echo t.ascii # prune(t.findNode("f")) # echo t.ascii # echo t.writeNewickString() -# # TODO: Write tests to ensure all of these fail. -# discard parseNewickString(Nd, "(B:1.0, [test]C:1.0)A:1.0;") -# discard parseNewickString(Nd, "(B:1.0,C:[test]1.0)A:1.0;") -# discard parseNewickString(Nd, "(B:1.0,C:1.0:[test])A:1.0;") # This is not caught as an exception -# discard parseNewickString(Nd, "B:1.0,C:1.0:[test])A:1.0;") # This is not caught as an exception -# discard parseNewickString(Nd, "B:1.0[test]") # This is not caught as an exception - -# echo parseNewickString(Nd, "B:1.0[test]").ascii - # type @@ -50,25 +48,15 @@ proc addChild*(parent, child: Nd) = # parent.children.add(child) # child.parent = parent -# proc writeAnnotation*[void](node: Nd[T]): string = -# result.add('[') -# result.add(node.data) -# result.add(']') +# proc writeNewickData*[void](node: Nd[T]): string = +# result = node.data -# proc parseAnnotation(node: Nd, str: string) = +# proc parseNewickData*[void](node: Nd[T], str: string) = # node.data = str # var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;", Nd) # echo t.writeNewickString(false) -# # Bad newick strings -# # TODO: Fix parser to catch these and raise exception with helpful error msg -# # var -# # str = "(B:1.0, [test]C:1.0)A:1.0;" -# # str = "(B:1.0,C:[test]1.0)A:1.0;" -# # str = "(B:1.0,C:1.0:[test])A:1.0;" -# # str = "B:1.0,C:1.0:[test])A:1.0;" -# # t = parseNewickString(str, Nd) # echo t.ascii # for i in t.preorder: From 8c7350b18975ba844690208cd1ffb8fb3c7f1873 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Wed, 27 Sep 2023 23:37:07 -0500 Subject: [PATCH 06/13] Working on reimplementation with concepts and rewriting parser with Npeg --- a.nim | 117 +++++++++++ b.nim | 3 + old-src/phylogeni.nim | 114 ++++++++++ old-src/phylogeni/drawing.nim | 78 +++++++ old-src/phylogeni/io/parseNewick.nim | 303 +++++++++++++++++++++++++++ old-src/phylogeni/io/writeNewick.nim | 38 ++++ old-src/phylogeni/simulate.nim | 82 ++++++++ old-src/phylogeni/tree.nim | 241 +++++++++++++++++++++ src/phylogeni/concepts.nim | 10 +- src/phylogeni/newickParser2.nim | 168 +++++++-------- src/test.nim | 76 ------- src/testNewick.nim | 55 +++++ 12 files changed, 1121 insertions(+), 164 deletions(-) create mode 100644 a.nim create mode 100644 b.nim create mode 100644 old-src/phylogeni.nim create mode 100644 old-src/phylogeni/drawing.nim create mode 100644 old-src/phylogeni/io/parseNewick.nim create mode 100644 old-src/phylogeni/io/writeNewick.nim create mode 100644 old-src/phylogeni/simulate.nim create mode 100644 old-src/phylogeni/tree.nim delete mode 100644 src/test.nim create mode 100644 src/testNewick.nim diff --git a/a.nim b/a.nim new file mode 100644 index 0000000..8ccf74e --- /dev/null +++ b/a.nim @@ -0,0 +1,117 @@ +# type +# Node = concept n, type T +# for i in n.children: +# i is T +# n.parent is T + +# Nd = ref object +# parent: Nd +# children: seq[Nd] + +# proc addChild(parent, child: Node) = +# parent.children.add(child) +# child.parent = parent + +# var +# a = Nd() +# b = Nd() +# a.addChild(b) +# echo a.children.len + + + +# import npeg + + +# Wrong number of arguments error +# proc test(T: typedesc, str: string) = +# let p = peg "start": +# start <- >"Test": +# echo $0 +# var m = p.match(str) +# echo m.captures +# test(string, "Test") + +# # Works +# import npeg +# proc test1(T: typedesc, str: string) = +# template `>`(a: untyped): untyped = discard +# let p = peg "start": +# start <- >"test" +# var m = p.match(str) +# echo m.captures +# test1(string, "test") + +# Error: Expected PEG rule name but got nnkSy +# import npeg +# proc test2(T: typedesc, str: string) = +# template `>`(a: untyped): untyped = discard +# let p = peg "start": +# test <- >"test" +# start <- test +# var m = p.match(str) +# echo m.captures +# test2(int, "test") + +# # Template does not interfere in non-generic case +# import npeg +# proc test3(str: string) = +# template `>`(a: untyped): untyped = discard +# let p = peg "start": +# test <- >"test" +# start <- test +# var m = p.match(str) +# echo m.captures +# test3("test") + + +# proc test(T: typedesc, str: string) = +# let p = peg "start": +# start <- "Test" +# var m = p.match(str) +# echo m.captures +# test(string, "Test") + +# proc test(obj: int, str: string) = +# let p = peg "start": +# start <- >"Test" +# var m = p.match(str) +# echo m.captures +# test(1, "Test") + +# proc test[T](obj: T, str: string) = +# let p = peg "start": +# start <- "Test" +# var m = p.match(str) +# echo m.captures +# test(1, "Test") + +import npeg + +proc parse*(T: typedesc, str:string) = + template `>`(a: untyped): untyped = discard + let p = peg "parser": + parser <- >"test" + let r = p.match(str) + # echo r.captures + +# parse(int, "test") + +# proc parse*(T: typedesc, str:string) = +# template `*`(a: untyped): untyped = discard +# template `>`(a: untyped): untyped = discard +# let p = peg "parser": +# elem <- internal +# internal <- '(' * >?elem * ')' +# parser <- >internal +# let r = p.match(str) +# echo r.captures +# parse(int, "(())") + +# proc parse*(str:string) = +# let p = peg "parser": +# elem <- internal +# internal <- '(' * ?elem * ')' +# parser <- internal +# let r = p.match(str) +# parse(str) \ No newline at end of file diff --git a/b.nim b/b.nim new file mode 100644 index 0000000..d382623 --- /dev/null +++ b/b.nim @@ -0,0 +1,3 @@ +import ./a + +parse(int, "test") \ No newline at end of file diff --git a/old-src/phylogeni.nim b/old-src/phylogeni.nim new file mode 100644 index 0000000..6a25368 --- /dev/null +++ b/old-src/phylogeni.nim @@ -0,0 +1,114 @@ +import ./phylogeni/[ + tree, + io/parseNewick, + io/writeNewick, + simulate + ] + +export tree, + parseNewick, + writeNewick, + simulate + +## ========= +## PhylogeNi +## ========= +## +## PhylogeNi is a Nim library for working with phylogenetic trees. +## + +runnableExamples: + var t = treeFromString("(B:1.0,C:1.0)A:1.0;") + + echo t + + # -A /-B + # \-C + + for i in t.preorder(): + if i.label == "C": + i.addChild(newNode("D", 1.0)) + i.addChild(newNode("E", 1.0)) + t.ladderize(Ascending) + echo t + + # /C /-D + # -A| \-E + # \-B + + var str = t.writeNewickString() + echo str + # [&U]((D:1.0,E:1.0)C:1.0,B:1.0)A:1.0; + +## +## See the module docs for more details: +## `tree<./phylogeni/tree.html>`_ +## Provides basic functions for working with `Tree` and `Node` types such as: +## - Tree and Node creation +## - Topology modification +## - Tree iteration +## +## `parseNewick<./phylogeni/io/parseNewick.html>`_ +## Provides functions for reading trees from files or strings. +## +## `writeNewick<./phylogeni/io/writeNewick.html>`_ +## Provides functions for writing trees to files or strings. +## +## `simulate<./phylogeni/tree.html>`_ +## Provides functions for simulating trees: +## - Pure birth model +## - Birth death model +## +## Generic Node Data +## ================= +## `Node` is a generic type which can have any object stored in the data field. +## +## One great feature of PhylogeNi is that you do not need to completely rewrite your +## own parser/writer for custom data types when reading and writing a newick file or string. +## You only need to create `parseAnnotation` and `writeAnnotation` procs to handle +## reading or writing the annotation string. + +runnableExamples: + import std/strutils + import std/strformat + + type + CustomData = object + posterior: float + credibleInterval: tuple[lower, upper: float] + + let treeStr = "(B:1.0[&p:0.95,ci:0.9-1.0],C:1.0[&p:0.95,ci:0.9-1.0])A:1.0[&p:0.95,ci:0.9-1.0];" + + proc parseAnnotation(p: var NewickParser[CustomData], annotation: string) = + let annotations = annotation.split(",") + var dataCheck = (p: false, ci: false) + for i in annotations: + let split = i.split(":") + doAssert split.len == 2 + case split[0] + of "p": + p.currNode.data.posterior = parseFloat(split[1]) + dataCheck.p = true + of "ci": + let ci = split[1].split("-") + doAssert ci.len == 2 + p.currNode.data.credibleInterval = (parseFloat(ci[0]), parseFloat(ci[1])) + dataCheck.ci = true + else: + raise newException(NewickError, "Invalid Annotation") + if not dataCheck.p or not dataCheck.ci: + raise newException(NewickError, "") + + proc writeAnnotation(node: Node[CustomData], str: var string) = + str.add(fmt"[&p:{$node.data.posterior},ci:{$node.data.credibleInterval.lower}-{$node.data.credibleInterval.upper}]") + + let + t = treeFromString(treeStr, CustomData) + str = t.writeNewickString() + echo str + # [&U](B:1.0[&p:0.95,ci:0.9-1.0],C:1.0[&p:0.95,ci:0.9-1.0])A:1.0[&p:0.95,ci:0.9-1.0]; + + + + + diff --git a/old-src/phylogeni/drawing.nim b/old-src/phylogeni/drawing.nim new file mode 100644 index 0000000..33e2e06 --- /dev/null +++ b/old-src/phylogeni/drawing.nim @@ -0,0 +1,78 @@ +# Inherit from Node and only borrow the necessary procs +# Leave out or change some setters and getters + +import ./tree + +import phylogeni + +type + DrawNode*[T] = ref object + x: float # Horizontal position of node, equivalent to node height + y: float # Vertical position of node + data: T + +# proc x*[T](n: DrawNode[T]): float = +# result = n.x + +# proc y*[T](n: DrawNode[T]): float = +# result = n.y + +# proc data*[T](n: DrawNode[T]): T = +# result = n.data + +template toClosure*(i): auto = + ## Wrap an inline iterator in a first-class closure iterator. + iterator j: type(i) {.closure.} = + for x in i: yield x + j + +proc copyToDrawNodeTree[T](tree: Node[T]): Node[DrawNode[T]] = + ## Copy tree structure and replace existing data with DrawNode type with + ## data being copied to the DrawNode data property + var copied = Node[DrawNode[T]](length:tree.length, label:tree.label, data:DrawNode[T](data:tree.data)) + for i in tree.children: + copied.addChild(copyToDrawNodeTree(i)) + result = copied + +proc getCoords*[T](tree: Node[T], branchLengthScaling=1.0, branchSepScaling=1.0): Node[DrawNode[T]] = + ## Return coordinates for a typical rectangular or slanted phylogeny + # TODO: Raise Error if branchLengthScaling or branchSepScaling is <=0 + var copied = copyToDrawNodeTree(tree) + + # Make newickorder a closure iterator using template + let newickOrderIt = toClosure(copied.newickorder) + + # Iter over nodes in newick order. Assign x on first pass of all nodes. + # Assign y when visiting leaves and second visit of each node. + var + root = newickOrderIt().node + leafY = 0.0 + root.data = DrawNode[T]() + root.data.x = root.length * branchSepScaling + for i in newickOrderIt(): + var n = i.node + if i.firstVisit: + # Assign x on first visit + n.data.x = n.parent.data.x + (n.length * branchLengthScaling) + # Assign y to leaves + if i.node.isLeaf: + n.data.y = leafY + leafY += branchSepScaling + else: + # Assign y on second visit of each internal node + if not n.isLeaf: + let + lo = n.children[0].data.y + up = n.children[^1].data.y + n.data.y = (up - lo) / 2 + lo + result = copied + +let t = parseNewickString("(B:1.0[Test],((E:1.0,F:1.0)D:1.0[Test],G:1.0)C:1.0)A:1.0;", typ=string) +let c = getCoords(t) +echo t.ascii +for i in c.preorder: + echo i.label, ", ", i.data.x, ", ", i.data.y +echo "" +let c2 = getCoords(t, branchLengthScaling=2.0, branchSepScaling=2.0) +for i in c2.preorder: + echo i.label, ", ", i.data.x, ", ", i.data.y diff --git a/old-src/phylogeni/io/parseNewick.nim b/old-src/phylogeni/io/parseNewick.nim new file mode 100644 index 0000000..f1555ac --- /dev/null +++ b/old-src/phylogeni/io/parseNewick.nim @@ -0,0 +1,303 @@ +# TODO: Should rewrite this a bit to be more constraining and to catch more errors +# before Nim does, such as when reading "A,B;":. Also regret allowing annotations +# to occur anywhere which will be problematic if I make trees generic and +# parseAnnotation mixins get called before the label and length is parsed. + +# TODO: String annotation is not currently being parsed + +import std/[streams, lexbase, strformat, strutils] +import ../tree + +type + NewickError* = object of IOError + + NewickState = enum + newickStart, newickTopology, newickLabel, newickLength, newickEnd, newickEOF + # TODO: This might be a better way to track state in order to raise errors if + # a newick string doesn't have any parentheses. Low priority given how + # unlikely that is. + # newickStart, newickStartLabel, newickStartLength, newickStartTopology, + # newickTopology, newickLabel, newickLength, newickEnd, newickEOF + + NewickParser*[T] = object of BaseLexer + root: Node[T] + currNode*: Node[T] + token: string + state: NewickState + annotationState: bool # False if an annotation has already been parsed + +const newickWhitespace = {' ', '\t', '\c', '\l'} + +proc raiseError[T](p: NewickParser[T], msg: string) = + var + lineNum = $p.lineNumber + colNum = $p.getColNumber(p.bufpos+1) + m = fmt"{msg} at line {lineNum}, column {colNum}" + raise newException(NewickError, m) + +proc parseWhitespace[T](p: var NewickParser[T], skip=true) = + while true: + case p.buf[p.bufpos] + of ' ', '\t': + if not skip: p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + of '\c': + if not skip: p.token.add(p.buf[p.bufpos]) + p.bufpos = lexbase.handleCR(p, p.bufpos) + of '\l': # same as \n + if not skip: p.token.add(p.buf[p.bufpos]) + p.bufpos = lexbase.handleLF(p, p.bufpos) + else: + break + +proc parseAnnotation(p: var NewickParser[string], annotation: string) = + p.currNode.data = annotation + +proc parseAnnotation(p: var NewickParser[void], annotation: string) = + discard + +proc parseBracket[T](p: var NewickParser[T], showComments=false) = + # TODO: handle unexpected end of file and newick statement + mixin parseAnnotation + p.token = "" + p.bufpos.inc() + while true: + case p.buf[p.bufpos] + of ']': + p.bufpos.inc() + break + of newickWhitespace: + p.parseWhitespace(skip=false) + else: + p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + if p.token.startswith('&'): + if p.annotationState: + p.parseAnnotation(p.token[1..^1]) + p.annotationState = false + else: + if showComments: + echo p.token + +proc parseLength[T](p: var NewickParser[T]) = + var parseLength = true + while true: + case p.buf[p.bufpos] + of '(', ',', ')', ';': + p.state = newickTopology + break + of newickWhitespace: + p.parseWhitespace() + of '[': + p.parseBracket() + of EndOfFile: + p.raiseError("Unexpected end of stream") + else: + if parseLength: + p.token = "" + while true: + case p.buf[p.bufpos] + of '(', ',', ')', ';', '[', newickWhitespace, EndOfFile: + parseLength = false + break + of '"': + p.raiseError("Unexpected \"") + else: + p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + p.currNode.length = parseFloat(p.token) + parseLength = false + +proc parseLabel[T](p: var NewickParser[T]) = + var parseLabel = true + p.annotationState = true + while true: + case p.buf[p.bufpos] + of '(', ',', ')', ';': + p.state = newickTopology + break + of ':': + p.state = newickLength + p.bufpos.inc() + break + of '[': + p.parseBracket() + of newickWhitespace: + p.parseWhitespace() + of EndOfFile: + p.raiseError("Unexpected end of stream") + of '"': + # Parse quoted text + if parseLabel: + p.token = "" + p.bufpos.inc() + while true: + case p.buf[p.bufpos] + of '"': + p.bufpos.inc() + break + of newickWhitespace: + p.parseWhitespace(skip=false) + else: + p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + p.currNode.label = p.token + parseLabel = false + else: + p.raiseError("Unexpected \"") + else: + # Parse unquoted text + if parseLabel: + p.token = "" + while true: + case p.buf[p.bufpos] + of '(', ',', ')', ';', ':', '[', ']', newickWhitespace, EndOfFile: + parseLabel = false + break + of '"': + p.raiseError("Unexpected \"") + else: + p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + p.currNode.label = p.token + parseLabel = false + else: + p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") + +proc parseTopology[T](p: var NewickParser[T]) = + # Parse newick tree + case p.buf[p.bufpos] + of '(': + var newNode = Node[T]() + p.currNode.addChild(newNode) + p.currNode = newNode + p.bufpos.inc() + p.state = newickLabel + of ',': + var newNode = Node[T]() + p.currNode.parent.addChild(newNode) + p.currNode = newNode + p.bufpos.inc() + p.state = newickLabel + of ')': + p.currNode = p.currNode.parent + p.bufpos.inc() + p.state = newickLabel + of ';': + if p.currNode == p.root: + p.bufpos.inc() + p.state = newickEnd + else: + p.raiseError("Mismatched parentheses") + else: + p.raiseError(&"Internal error, report possible bug") + +proc parseStart[T](p: var NewickParser[T]) = + # Parse beginning of newick file + while true: + case p.buf[p.bufpos] + of '(': + p.state = newickTopology + break + of ',': + p.raiseError("Unexpected comma. There can be only one root node.") + of newickWhitespace: + p.parseWhitespace() + of '[': + if p.buf[p.bufpos+1] == '&': + case p.buf[p.bufpos+2] + of 'r', 'R': + # p.tree.rooted = true + discard + of 'u', 'U': + # p.tree.rooted = false + discard + else: + p.bufpos.inc(2) + p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") + if p.buf[p.bufpos+3] == ']': + p.bufpos.inc(4) + else: + p.bufpos.inc(3) + p.raiseError("Expected \"]\"") + else: + p.parseBracket() + of EndOfFile: + # p.state = newickEOF + # break + p.raiseError("Unexpected end of file. No newick statment found.") + else: + p.state = newickLabel + break + +proc parseTree[T](p: var NewickParser[T]) = + p.parseWhitespace() + while true: + case p.state + of newickStart: + p.parseStart() + of newickTopology: + p.parseTopology() + of newickLabel: + p.parseLabel() + of newickLength: + p.parseLength() + of newickEnd: + break + of newickEOF: + break + +proc parseNewickStream*(stream: Stream, typ: typedesc = void): Node[typ] = + ## Parse a newick stream + var + p = NewickParser[typ]() + p.root = Node[typ]() + p.currNode = p.root + p.open(stream) + p.parseTree() + p.close() + result = p.root + +# proc parseNewickStream*[T](treeSeq: var TreeSeq[T], stream: Stream) = +# ## Parse a newick stream +# var +# p = NewickParser[T]() +# p.open(stream) +# while true: +# p.state = newickStart +# p.tree = Tree[T]() +# p.tree.root = Node[T]() +# p.currNode = p.tree.root +# p.parseTree() +# case p.state +# of newickEOF: +# break +# of newickEnd: +# treeSeq.add(p.tree) +# else: +# p.raiseError("Internal error, report possible bug") +# p.close() + +proc parseNewickString*(str: string, typ: typedesc = void): Node[typ] = + ## Parse a newick string + var ss = newStringStream(str) + result = parseNewickStream(ss, typ) + ss.close() + +# proc parseNewickString*[T](treesSeq: var TreeSeq[T], str: string) = +# ## Parse a newick string +# var ss = newStringStream(str) +# treesSeq.parseNewickStream(ss) +# ss.close() + +proc parseNewickFile*(path: string, typ: typedesc = void): Node[typ] = + ## Parse a newick file + var fs = newFileStream(path, fmRead) + result = parseNewickStream(fs, typ) + fs.close() + +# proc parseNewickFile*[T](treeSeq: var TreeSeq[T], path: string) = +# ## Parse a newick file +# var fs = newFileStream(path, fmRead) +# treeSeq.parseNewickStream(fs) +# fs.close() \ No newline at end of file diff --git a/old-src/phylogeni/io/writeNewick.nim b/old-src/phylogeni/io/writeNewick.nim new file mode 100644 index 0000000..88735a2 --- /dev/null +++ b/old-src/phylogeni/io/writeNewick.nim @@ -0,0 +1,38 @@ +import ../tree +import std/strformat + +func writeNewickData(node: Node[string], str: var string) = + str.add(fmt"[&{node.data}]") + +func writeNewickData(node: Node[void], str: var string) = + discard + +func writeNewickData[T](node: Node[T], str: var string) = + mixin writeAnnotation + str.add(node.label) + str.add(fmt":{$node.length}") + node.writeAnnotation(str) + +func writeNewickString*[T](tree: Node[T]): string = + ## Write newick string for Node object + var str = "" + for i in tree.newickorder(): + if i.firstVisit == true: + if i.node.isLeaf(): + str.add(i.node.writeNewickData) + if i.node != i.node.parent.children[^1]: # not the first node in parents children + str.add(",") + else: # is internal node + str.add("(") + else: # is second visit to node + str.add(")") + str.add(i.node.writeNewickData) + if (i.node != tree) and (i.node != i.node.parent.children[^1]): # is not last node in parents children + str.add(",") + str.add(";") + result = str + +proc writeNewickFile*[T](tree: Node[T], filename:string) = + # Write a newick file for Node object + var str = writeNewickString(tree) + writeFile(filename, str) diff --git a/old-src/phylogeni/simulate.nim b/old-src/phylogeni/simulate.nim new file mode 100644 index 0000000..5be4255 --- /dev/null +++ b/old-src/phylogeni/simulate.nim @@ -0,0 +1,82 @@ +import std/[random, math] +import ./tree + +# TODO: Make BirthDeath Simulator Work +# TODO: Make option to take random number generator object as an option + +proc randExp(l: float): float = + -ln(rand(1.0))/l + +proc uniformPureBirth*(nTips: int, birthRate: float=1.0, typ=void): Node[typ] = + ## Simulate tree under uniform pure birth process. + var + t = Node[typ]() + leaves = @[t] + for i in 1 ..< nTips: + var + waitTime = randExp(float(leaves.len()) * birthRate) + rLeaf = rand(leaves.len - 1) + # Add wait time to all leaves + for node in leaves: + node.length += waitTime + # Add descendant nodes to random leaf + for i in 0..1: + var nd = Node[typ]() + leaves[rLeaf].addChild(nd) + leaves.add(nd) + # Remove previous random leaf from leaf list since it is now internal node + leaves.delete(rLeaf) + # Add additional length and tip labels to final leaves + var + waitTime = randExp(float(leaves.len()) * birthRate) + inc = 1 + for node in leaves: + node.length += waitTime + node.label = "T" & $inc + inc += 1 + result = t + +proc uniformBirthDeath*(nTips: int, birthRate=1.0, deathRate=1.0, rerun=false, typ=void): Node[typ] = + ## Simulate tree under uniform birth death process. + var + t = Node[typ]() + leaves = @[t] + while true: + if leaves.len() == nTips: + break + var + waitTime = randExp(float(leaves.len()) * (birthRate + deathRate)) + rLeaf = rand(leaves.len - 1) + # Add wait time to all leaves + for node in leaves: + node.length += waitTime + # Determine if speciation or extinction even + if rand(1.0) < birthRate / (birthRate + deathRate): + # Speciation event + for i in 0..1: + var nd = Node[typ]() + leaves[rLeaf].addChild(nd) + leaves.add(nd) + else: + # Extinction event + if leaves.len() == 1: + # Rerun + if rerun: + leaves.add(t) + # Or quit + else: + break + else: + t.prune(leaves[rLeaf]) + # Delete random leaf from leaf list + leaves.delete(rLeaf) + # Add additional length and tip labels to final leaves + var + waitTime = randExp(float(leaves.len()) * birthRate) + inc = 1 + for node in leaves: + node.length += waitTime + node.label = "T" & $inc + inc += 1 + result = t + diff --git a/old-src/phylogeni/tree.nim b/old-src/phylogeni/tree.nim new file mode 100644 index 0000000..c9c8ea2 --- /dev/null +++ b/old-src/phylogeni/tree.nim @@ -0,0 +1,241 @@ +#TODO: Make Node attributes private and make setters and getters +# or make Node a concept + +import std/[algorithm, tables, hashes, strutils, sequtils] + +export algorithm.SortOrder + +type + Node*[T] = ref object + parent*: Node[T] + children*: seq[Node[T]] + label*: string + length*: float + data*: T + + TreeError* = object of CatchableError + +func hash*[T](n: Node[T]): Hash = + result = n.label.hash !& n.length.hash + result = !$result + +func addChild*[T](parent: Node[T], newChild: Node[T]) = + ## Add child node to parent. + newChild.parent = parent + parent.children.add(newChild) + +func addSister*[T](node: Node[T], newSister: Node[T]) = + ## Add sister node. + newSister.parent = node.parent + node.parent.children.add(newSister) + +func isLeaf*[T](node: Node[T]): bool = + ## Check if node is leaf. + if node.children.len == 0: + result = true + else: + result = false + +func isRoot*[T](node: Node[T]): bool = + if node.parent == nil: + result = true + else: + result = false + +func prune*[T](tree, node: Node[T]) = + ## Prune branch leading to node from tree. + if node.parent == nil: + raise newException(TreeError, "Cannot prune root node") + var parent = node.parent + parent.children.delete(parent.children.find(node)) + if parent.children.len() == 1: + var child = parent.children[0] + parent.length += child.length + parent.children = child.children + parent.label = child.label + +proc copyTree*[T](tree: Node[T], typ: typedesc = void): Node[typ] = + ## Copy the structure, edge lengths, and labels of a tree. The returned tree + ## may have a different data type. + var copied = Node[typ](length:tree.length, label:tree.label) + for i in tree.children: + copied.addChild(copyTree(i, typ)) + result = copied + +iterator preorder*[T](root: Node[T]): Node[T] = + ## Preorder traverse. + var stack = @[root] + while stack.len > 0: + var node = stack.pop() + stack.add(node.children.reversed()) + yield node + +iterator postorder*[T](root: Node[T]): Node[T] = + ## Postorder traverse. + var + preStack = @[root] + postStack: seq[Node[T]] + while preStack.len > 0: + var node = preStack.pop() + postStack.add(node) + preStack.add(node.children) + while postStack.len > 0: + var node = postStack.pop() + yield node + +iterator newickorder*[T](root: Node[T]): tuple[node:Node[T], firstVisit:bool] = + ## Newick order traverse. All internal nodes are visited twice. + var stack: seq[tuple[node: Node[T], firstVisit: bool]] + stack.add((node: root, firstVisit: false)) + stack.add((node: root, firstVisit: true)) + while stack.len > 0: + var nodeTuple = stack.pop() + yield (nodeTuple) + if nodeTuple.node.children.len > 0: + if nodeTuple.firstVisit == true: + for child in nodeTuple.node.children.reversed: + if child.children.len > 0: + stack.add((child, false)) + stack.add((child, true)) + else: + stack.add((child, true)) + +iterator levelorder*[T](root: Node[T]): Node[T] = + ## Levelorder traverse. + yield root + var stack = root.children + while stack.len > 0: + var node = stack[0] + stack.delete(0) + yield node + stack.add(node.children) + +iterator iterleaves*[T](root: Node[T]): Node[T] = + ## Iter over leaves. + for i in root.preorder(): + if i.is_leaf(): + yield i + +func ladderize*[T](root: Node[T], order: SortOrder = Ascending) = + ## Ladderize subtree. + # TODO: Should reimplement with heap queue and without using table + var + nodeDescendantCount = initTable[Node[T], int]() + for node in root.postorder(): + if node.children.len == 0: + nodeDescendantCount[node] = 0 + else: + var total = 0 + for child in node.children: + total += nodeDescendantCount[child] + total += node.children.len + nodeDescendantCount[node] = total + node.children.sort( + cmp=func(a, b: Node[T]): int = cmp(nodeDescendantCount[b], + nodeDescendantCount[a]), order=order) + +func calcTreeLength*[T](node: Node[T]): float = + ## Calculate total length of tree. + result = 0.0 + for child in node.children: + for i in child.preorder(): + result += i.length + +func treeHeight*[T](node: Node[T]): float = + ## Calculate the height of subtree. + var maxHeight = 0.0 + for child in node.children: + let childHeight = treeHeight(child) + maxHeight = max(maxHeight, childHeight) + result = maxHeight + node.length + +func findNode*[T](tree: Node[T], str: string): Node[T] = + ## Returns first instance of node label matching str. + for i in tree.preorder: + if i.label == str: + return i + +func getAncestors*[T](node: Node[T]): seq[Node[T]] = + var curr = node + while true: + if curr.parent != nil: + result.add(curr.parent) + curr = curr.parent + else: + break + +func getMRCA*[T](a, b: Node[T]): Node[T] = + ## Get the most recent common ancestor of two nodes. + # TODO: I think this could be faster adding the elements of the shoter list to a + # hash set and then checking if the elements of the other list belong to that set + let + aAncestors = a.getAncestors + bAncestors = b.getAncestors + for i in aAncestors: + for j in bAncestors: + if i == j: + return i + raise newException(TreeError, "No MRCA shared by nodes") + +func get_ascii[T](node: Node[T], char1="-", showInternal=true): tuple[clines: seq[string], mid:int] = + ## Generates ascii string representation of tree. + var + len = 3 + if node.children.len == 0 or showInternal == true: + if node.label.len > len: + len = node.label.len + var + pad = strutils.repeat(' ', len) + pa = strutils.repeat(' ', len-1) + if node.children.len > 0: + var + mids: seq[int] + results: seq[string] + for child in node.children: + var char2: string + if node.children.len == 1: + char2 = "-" + elif child == node.children[0]: + char2 = "/" + elif child == node.children[^1]: + char2 = "\\" + else: + char2 = "-" + var (clines, mid) = get_ascii(child, char2, showInternal) + mids.add(mid+len(results)) + results.add(clines) + var + lo = mids[0] + hi = mids[^1] + last = len(results) + mid = int((lo+hi)/2) + prefixes: seq[string] + prefixes.add(sequtils.repeat(pad, lo+1)) + if mids.len > 1: + prefixes.add(sequtils.repeat(pa & "|", hi-lo-1)) + prefixes.add(sequtils.repeat(pad, last-hi)) + prefixes[mid] = char1 & strutils.repeat("-", len-2) & prefixes[mid][^1] + var new_results: seq[string] + for (p, r) in zip(prefixes, results): + new_results.add(p&r) + if showInternal: + var stem = new_results[mid] + new_results[mid] = stem[0] & node.label & stem[node.label.len+1..^1] + result = (new_results, mid) + else: + result = (@[char1 & "-" & node.label], 0) + +func ascii*[T](node: Node[T], char1="-", showInternal=true): string = + ## Returns ascii string representation of tree. + var (lines, _) = get_ascii(node, char1, showInternal) + result = lines.join("\n") + +func `$`*[T](node: Node[T]): string = + result = node.label + +# TODO: Implement these: +# func delete*(node: Node) = + ## Remove only this node and not parent or children + +# func extractTreeCopy*[T](node: Node[T]): Node[T] = + # Return copy of tree rooted at node. \ No newline at end of file diff --git a/src/phylogeni/concepts.nim b/src/phylogeni/concepts.nim index 7f0228b..007945b 100644 --- a/src/phylogeni/concepts.nim +++ b/src/phylogeni/concepts.nim @@ -141,7 +141,7 @@ func treeHeight*(node: LengthNode): float = ############################### -# Data Node +# type ReadableDataNode* = concept n n is TraversableNode @@ -152,6 +152,14 @@ type n is TraversableNode n.writeNewickData is string +# TODO: would this be redundant? +# TODO: could it improve clarity? +# For use by procs in manipulate module +# type +# MutableNode* = concept n +# n is TraversableNode +# n.parent #TODO: how to confirm if this is mutable +# n.children # TODO: how to confirm that this is mutable diff --git a/src/phylogeni/newickParser2.nim b/src/phylogeni/newickParser2.nim index 8c29576..1fca0a8 100644 --- a/src/phylogeni/newickParser2.nim +++ b/src/phylogeni/newickParser2.nim @@ -1,29 +1,3 @@ - -# TODO: Remove this once generics are fixed -type - Nd* = ref object - parent*: Nd - children*: seq[Nd] - label*: string - length*: float - data*: string - -proc addChild*(parent, child: Nd) = - parent.children.add(child) - child.parent = parent - -proc parseNewickData*(n: Nd, data: string) = - n.data = data - - - -################################################################################ -# New parser -#TODO: Make parser accept generics once bug is fixed -# https://github.com/zevv/npeg/issues/68 -# https://github.com/nim-lang/Nim/issues/22740 - - import npeg import ./concepts import std/[strutils, strformat] @@ -31,29 +5,26 @@ import std/[strutils, strformat] type NewickError* = object of IOError -# proc newChildNode[T](curr: var T) = -proc newChildNode(curr: var Nd) = - var newNode = Nd() +proc newChildNode[T](curr: var T) = + mixin addChild + var newNode = new(T) curr.addChild(newNode) curr = newNode -# proc newSisterNode[T](curr: var T) = -proc newSisterNode(curr: var Nd) = - var newNode = Nd() +proc newSisterNode[T](curr: var T) = + mixin addChild + var newNode = new(T) curr.parent.addChild(newNode) curr = newNode -# proc branchTerminated[T](curr: var T) = -proc branchTerminated(curr: var Nd) = +proc branchTerminated[T](curr: var T) = curr = curr.parent -# proc parseLabel[T](curr: var T, label: string) = -proc parseLabel(curr: var Nd, label: string) = +proc parseLabel[T](curr: var T, label: string) = when curr is LabeledNode: curr.label = label -# proc parseLength[T](curr: var T, length: string) = -proc parseLength(curr: var Nd, length: string) = +proc parseLength[T](curr: var T, length: string) = # TODO: Handle errors parsing int and float when curr is LengthNode: if length.len > 0: @@ -62,49 +33,54 @@ proc parseLength(curr: var Nd, length: string) = when curr.length is float: curr.length = parseFloat(length) -# proc parseData[T](curr: var T, data: string) = -proc parseData(curr: var Nd, data: string) = - when curr.is ReadableDataNode: - # mixin parseNewickData +proc parseData[T](curr: var T, data: string) = + when curr is ReadableDataNode: + mixin parseNewickData parseNewickData(curr, data) -# proc parseNewickString*(T: typedesc[TraversableNode], str:string): T = -proc parseNewickString*(str:string): Nd = +template genericBugWorkAround() = + # Template definitions as workaround for bug in Nim + # https://github.com/zevv/npeg/issues/68 + # https://github.com/nim-lang/Nim/issues/22740 + template `>`(a: untyped): untyped = discard + template `*`(a: untyped): untyped = discard + template `-`(a: untyped): untyped = discard + template `+`(a: untyped): untyped = discard + # template `?`(a: untyped): untyped = discard + # template `!`(a: untyped): untyped = discard + template `$`(a: untyped): untyped = discard + +proc parseNewickString*(T: typedesc[TraversableNode], str:string): T = + genericBugWorkAround() var - # root = new(T) - root = new(Nd) + root = new(T) curr = root - dataState = true let p = peg "newick": - # TODO: How to move this elsewhere or even simplify?: - dataChars <- Print - {'[', ']'} - S <- *Space - comment <- ?('[' * >*dataChars * ']') - # TODO: Why doesn't this work? - # nestComment <- >('[' * *(dataChars | nestComment ) * ']') - # comment <- ?('[' * >*(dataChars | nested) * ']') - stop <- ';' - lBrack <- '(' : - newChildNode(curr) - rBrack <- ')' : - branchTerminated(curr) - comma <- ',' : - newSisterNode(curr) - label <- >+(Alnum | '_'): - parseLabel(curr, $1) - length <- ':' * >?(+Digit * ?('.' * +Digit)): - parseLength(curr, $1) - data <- '[' * >*dataChars * ']': - parseData(curr, $1) - annotation <- ?data * S * ?label * S * ?data * S * ?length * S * ?data: - dataState=true - leaf <- annotation - branchset <- (internal | leaf) * S * *(comma * S * (internal | leaf)) - internal <- S * lBrack * S * ?branchset * S * rBrack * S * annotation - newick <- S * comment * (internal | leaf) * S * stop * S * !1 + dataChars <- Print - {'[', ']'} + S <- *Space + nComment <- >('[' * *(nComment | dataChars) * ']') + comment <- '[' * >*(nComment | dataChars) * ']' + stop <- ';' + lBrack <- '(': + newChildNode(curr) + rBrack <- ')': + branchTerminated(curr) + comma <- ',': + newSisterNode(curr) + label <- >+(Alnum | '_'): + parseLabel(curr, $1) + length <- ':' * >?(+Digit * ?('.' * +Digit)): + parseLength(curr, $1) + data <- >comment: + parseData(curr, $1) + annotation <- ?data * S * ?label * S * ?data * S * ?length * S * ?data + leaf <- annotation + branchset <- (internal | leaf) * S * *(comma * S * (internal | leaf)) + internal <- S * lBrack * S * ?branchset * S * rBrack * S * annotation + start <- *( Space | comment ) + newick <- start * (internal | leaf) * S * stop * S * !1 let r = p.match(str) - echo r if not r.ok: var msg = &"Unexpected '{str[r.matchMax]}' at position {r.matchMax} of Newick string. Problem may originate before this position." raise newException(NewickError, msg) @@ -113,25 +89,15 @@ proc parseNewickString*(str:string): Nd = raise newException(NewickError, msg) result = root -# proc parseNewickFile*(T: typedesc[TraversableNode], path: string): T = -proc parseNewickFile*(path: string): Nd = +proc parseNewickFile*(T: typedesc[TraversableNode], path: string): T = var str = readFile(path) - result = parseNewickString(str) + result = parseNewickString(T, str) # ################################################### # # Testing -# var -# str = "(A:1.0[Test],B,(C,D));" -# t = parseNewickString(str) -# echo t.ascii - -# # import ./traverse -# # for i in t.preorder: -# # echo i[] - # discard parseNewickString("(,,(,));") # discard parseNewickString("(A,B,(C,D));") # discard parseNewickString("(A,B,(C,D)E)F;") @@ -141,4 +107,32 @@ proc parseNewickFile*(path: string): Nd = # discard parseNewickString("(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F;") # discard parseNewickString("((B:0.2,(C:0.3,D:0.4)E:0.5)F:0.1)A;") # # # TODO: Make test cases with data annotation -# # # TODO: Make test cases expected to fail \ No newline at end of file +# # # TODO: Make test cases expected to fail + + + +type + Nd* = ref object + parent*: Nd + children*: seq[Nd] + label*: string + length*: float + data*: string + +proc addChild*(parent, child: Nd) = + ## A bug in Nim currently requires that each type matching that is + ## a TraversableNode must have an addChild proc written for it. + ## This will no longer be necesary when the bug is fixed + ## https://github.com/nim-lang/Nim/issues/22723 + # TODO: Make this a concept once that works + parent.children.add(child) + child.parent = parent + +proc parseNewickData*(n: Nd, data: string) = + n.data = data + +proc writeNewickData*(n: Nd): string = + n.data + +var t = parseNewickString(Nd, "[[Test]]((([Test]f:1.0[Test[Test]],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") +echo t.ascii \ No newline at end of file diff --git a/src/test.nim b/src/test.nim deleted file mode 100644 index db5ac8c..0000000 --- a/src/test.nim +++ /dev/null @@ -1,76 +0,0 @@ -import ./phylogeni - -# TODO: This code is currently copied to newickParser2 until bug with parser is resolved -# type -# Nd* = ref object -# parent*: Nd -# children*: seq[Nd] -# label*: string -# length*: float - -# proc addChild*(parent, child: Nd) = -# ## A bug in Nim currently requires that each type matching that is -# ## a TraversableNode must have an addChild proc written for it. -# ## This will no longer be necesary when the bug is fixed -# ## https://github.com/nim-lang/Nim/issues/22723 -# # TODO: Make this a concept once that works -# parent.children.add(child) -# child.parent = parent - -# proc parseNewickData*(n: Nd, data: string) = -# n.data = data - -proc writeNewickData*(n: Nd): string = - n.data - -var t = parseNewickString("((([]f:1.0[Test],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") -echo t.ascii -# for i in t.preorder: -# echo i[] -# t.ladderize() -# echo t.ascii -# prune(t.findNode("f")) -# echo t.ascii -# echo t.writeNewickString() - - - -# type -# Nd*[T] = ref object -# parent*: Nd[T] -# children*: seq[Nd[T]] -# label*: string -# length*: float -# data*: T - -# proc addChild*[T](parent, child: Nd[t]) = -# # TODO: Make this a concept once that works -# parent.children.add(child) -# child.parent = parent - -# proc writeNewickData*[void](node: Nd[T]): string = -# result = node.data - -# proc parseNewickData*[void](node: Nd[T], str: string) = -# node.data = str - -# var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;", Nd) -# echo t.writeNewickString(false) - - -# echo t.ascii -# for i in t.preorder: -# echo i.label -# echo "" - -# var c = t.getCoords() -# for i in c.preorder(): -# echo i[] -# echo "" - -# t.ladderize(Descending) -# echo t.ascii -# var c2 = t.getCoords() -# for i in c2.preorder(): -# echo i[] - diff --git a/src/testNewick.nim b/src/testNewick.nim new file mode 100644 index 0000000..d4c566e --- /dev/null +++ b/src/testNewick.nim @@ -0,0 +1,55 @@ +import ./phylogeni + +type + Nd* = ref object + parent*: Nd + children*: seq[Nd] + label*: string + length*: float + data*: string + +proc addChild*(parent, child: Nd) = + ## A bug in Nim currently requires that each type matching that is + ## a TraversableNode must have an addChild proc written for it. + ## This will no longer be necesary when the bug is fixed + ## https://github.com/nim-lang/Nim/issues/22723 + # TODO: Make this a concept once that works + parent.children.add(child) + child.parent = parent + +proc parseNewickData*(n: Nd, data: string) = + n.data = data + +proc writeNewickData*(n: Nd): string = + n.data + +# var t = parseNewickString(Nd, "((([Test]f:1.0[Test[Test]],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") +var t = parseNewickString(Nd, "(())") +# echo t.ascii +# for i in t.preorder: +# echo i[] +# t.ladderize() +# echo t.ascii +# prune(t.findNode("f")) +# echo t.ascii +# echo t.writeNewickString() + + + + +# echo t.ascii +# for i in t.preorder: +# echo i.label +# echo "" + +# var c = t.getCoords() +# for i in c.preorder(): +# echo i[] +# echo "" + +# t.ladderize(Descending) +# echo t.ascii +# var c2 = t.getCoords() +# for i in c2.preorder(): +# echo i[] + From cfbc0b5990dac9f6af967de9116ab39e24c3d647 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Thu, 28 Sep 2023 17:45:25 -0500 Subject: [PATCH 07/13] Cleaning up and simplifying newickParser tests --- a.nim | 98 +------- b.nim | 3 +- src/phylogeni.nim | 14 +- src/phylogeni/newickParser.nim | 401 +++++++++--------------------- src/phylogeni/newickParser2.nim | 138 ---------- src/phylogeni/newickParserOld.nim | 284 +++++++++++++++++++++ src/testNewick.nim | 80 +++--- 7 files changed, 455 insertions(+), 563 deletions(-) delete mode 100644 src/phylogeni/newickParser2.nim create mode 100644 src/phylogeni/newickParserOld.nim diff --git a/a.nim b/a.nim index 8ccf74e..33f85a0 100644 --- a/a.nim +++ b/a.nim @@ -1,101 +1,21 @@ -# type -# Node = concept n, type T -# for i in n.children: -# i is T -# n.parent is T - -# Nd = ref object -# parent: Nd -# children: seq[Nd] - -# proc addChild(parent, child: Node) = -# parent.children.add(child) -# child.parent = parent - -# var -# a = Nd() -# b = Nd() -# a.addChild(b) -# echo a.children.len - - - -# import npeg - - -# Wrong number of arguments error -# proc test(T: typedesc, str: string) = -# let p = peg "start": -# start <- >"Test": -# echo $0 -# var m = p.match(str) -# echo m.captures -# test(string, "Test") - -# # Works -# import npeg -# proc test1(T: typedesc, str: string) = -# template `>`(a: untyped): untyped = discard -# let p = peg "start": -# start <- >"test" -# var m = p.match(str) -# echo m.captures -# test1(string, "test") - -# Error: Expected PEG rule name but got nnkSy -# import npeg -# proc test2(T: typedesc, str: string) = -# template `>`(a: untyped): untyped = discard -# let p = peg "start": -# test <- >"test" -# start <- test -# var m = p.match(str) -# echo m.captures -# test2(int, "test") +import npeg -# # Template does not interfere in non-generic case -# import npeg -# proc test3(str: string) = +# proc parse*(T: typedesc, str:string) = # template `>`(a: untyped): untyped = discard -# let p = peg "start": -# test <- >"test" -# start <- test -# var m = p.match(str) -# echo m.captures -# test3("test") - - -# proc test(T: typedesc, str: string) = -# let p = peg "start": -# start <- "Test" -# var m = p.match(str) -# echo m.captures -# test(string, "Test") - -# proc test(obj: int, str: string) = -# let p = peg "start": -# start <- >"Test" -# var m = p.match(str) -# echo m.captures -# test(1, "Test") - -# proc test[T](obj: T, str: string) = -# let p = peg "start": -# start <- "Test" -# var m = p.match(str) -# echo m.captures -# test(1, "Test") - -import npeg +# let p = peg "parser": +# parser <- >"test" +# let r = p.match(str) +# # echo r.captures +# parse(int, "test") -proc parse*(T: typedesc, str:string) = +proc parse*[T](o: T, str:string) = template `>`(a: untyped): untyped = discard let p = peg "parser": parser <- >"test" let r = p.match(str) # echo r.captures +# parse(1, "test") -# parse(int, "test") # proc parse*(T: typedesc, str:string) = # template `*`(a: untyped): untyped = discard diff --git a/b.nim b/b.nim index d382623..c45b550 100644 --- a/b.nim +++ b/b.nim @@ -1,3 +1,4 @@ import ./a -parse(int, "test") \ No newline at end of file +# parse(int, "test") +parse(1, "test") \ No newline at end of file diff --git a/src/phylogeni.nim b/src/phylogeni.nim index 0f65088..ecdc387 100644 --- a/src/phylogeni.nim +++ b/src/phylogeni.nim @@ -2,7 +2,7 @@ import ./phylogeni/[ concepts, coordinates, manipulate, - newickParser2, + newickParser, newickWriter, traverse] @@ -10,6 +10,14 @@ export concepts, coordinates, manipulate, - newickParser2, + newickParser, newickWriter, - traverse \ No newline at end of file + traverse + +# include ./phylogeni/[ +# concepts, +# coordinates, +# manipulate, +# newickParser, +# newickWriter, +# traverse] \ No newline at end of file diff --git a/src/phylogeni/newickParser.nim b/src/phylogeni/newickParser.nim index 6705f95..c71af12 100644 --- a/src/phylogeni/newickParser.nim +++ b/src/phylogeni/newickParser.nim @@ -1,284 +1,125 @@ -#TODO: Should rewrite this using a parser library, it has gotten too complex +import npeg +import ./concepts +import std/[strutils, strformat] -import ./concepts, ./traverse -import std/[streams, lexbase, strformat, strutils] - -type +type NewickError* = object of IOError - NewickState = enum - newickStart, newickTopology, newickLabel, newickLength, newickAnnotation, - newickEnd, newickEOF - # TODO: This might be a better way to track state in order to raise errors if - # a newick string doesn't have any parentheses. Low priority given how - # unlikely that is. - # newickStart, newickStartLabel, newickStartLength, newickStartTopology, - # newickTopology, newickLabel, newickLength, newickEnd, newickEOF +proc newChildNode[T](curr: var T) = + mixin addChild + var newNode = new(T) + curr.addChild(newNode) + curr = newNode + +proc newSisterNode[T](curr: var T) = + mixin addChild + var newNode = new(T) + curr.parent.addChild(newNode) + curr = newNode + +proc branchTerminated[T](curr: var T) = + curr = curr.parent + +proc parseLabel[T](curr: var T, label: string) = + when curr is LabeledNode: + curr.label = label + +proc parseLength[T](curr: T, length: string) = + # TODO: Handle errors parsing int and float + when curr is LengthNode: + if length.len > 0: + when curr.length is int: + curr.length = parseInt(length) + when curr.length is float: + curr.length = parseFloat(length) + +proc parseData[T](curr: var T, data: string) = + when curr is ReadableDataNode: + mixin parseNewickData + parseNewickData(curr, data) + +template genericBugWorkAround() = + # Template definitions as workaround for bug in Nim + # https://github.com/zevv/npeg/issues/68 + # https://github.com/nim-lang/Nim/issues/22740 + template `>`(a: untyped): untyped = discard + template `*`(a: untyped): untyped = discard + template `-`(a: untyped): untyped = discard + template `+`(a: untyped): untyped = discard + # template `?`(a: untyped): untyped = discard + # template `!`(a: untyped): untyped = discard + template `$`(a: untyped): untyped = discard - NewickParser[T: TraversableNode] = object of BaseLexer - root: T - currNode: T - token: string - state: NewickState - annotationState: bool # False if an annotation has already been parsed - -const newickWhitespace = {' ', '\t', '\c', '\l'} - -proc raiseError(p: NewickParser, msg: string) = - var - lineNum = $p.lineNumber - colNum = $p.getColNumber(p.bufpos+1) - m = fmt"{msg} at line {lineNum}, column {colNum}" - raise newException(NewickError, m) - -proc parseWhitespace(p: var NewickParser, skip=true) = - while true: - case p.buf[p.bufpos] - of ' ', '\t': - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - of '\c': - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos = lexbase.handleCR(p, p.bufpos) - of '\l': # same as \n - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos = lexbase.handleLF(p, p.bufpos) - else: - break - -# # proc parseAnnotation(p: var NewickParser[string], annotation: string) = -# # p.currNode.data = annotation - -# # proc parseAnnotation(p: var NewickParser[void], annotation: string) = -# # discard - -proc parseBracket(p: var NewickParser) = - # TODO: handle unexpected end of file and newick statement - mixin parseAnnotation - p.token = "" - p.bufpos.inc() - while true: - case p.buf[p.bufpos] - of ']': - p.bufpos.inc() - break - of newickWhitespace: - p.parseWhitespace(skip=false) - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - if p.token.startswith('&'): - if p.annotationState: - # p.parseAnnotation(p.token[1..^1]) - p.annotationState = false - -proc parseLength[T](p: var NewickParser[T]) = - #TODO: Determine if length is float or int for nodetype and convert string appropriately - var parseLength = true - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';': - p.state = newickTopology - break - of newickWhitespace: - p.parseWhitespace() - of '[': - # p.parseBracket() - p.state = newickAnnotation - break - of EndOfFile: - p.raiseError("Unexpected end of stream") - else: - if parseLength: - p.token = "" - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';', '[', newickWhitespace, EndOfFile: - parseLength = false - break - of '"': - p.raiseError("Unexpected \"") - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.length = parseFloat(p.token) - parseLength = false - -proc parseLabel(p: var NewickParser) = - # TODO: Write when statement to determine if node has label property - var parseLabel = true - p.annotationState = true - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';': - p.state = newickTopology - break - of ':': - p.state = newickLength - p.bufpos.inc() - break - of '[': - p.state = newickAnnotation - break - of newickWhitespace: - p.parseWhitespace() - of EndOfFile: - p.raiseError("Unexpected end of stream") - of '"': - # Parse quoted text - if parseLabel: - p.token = "" - p.bufpos.inc() - while true: - case p.buf[p.bufpos] - of '"': - p.bufpos.inc() - break - of newickWhitespace: - p.parseWhitespace(skip=false) - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.label = p.token - parseLabel = false - else: - p.raiseError("Unexpected \"") - else: - # Parse unquoted text - if parseLabel: - p.token = "" - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';', ':', '[', ']', newickWhitespace, EndOfFile: - parseLabel = false - break - of '"': - p.raiseError("Unexpected \"") - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.label = p.token - parseLabel = false - else: - p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - -# proc skipLabel(p: var NewickParser) = -# while true: -# case p.buf[p.bufpos] -# of - -proc parseData[T](p: var NewickParser[T]) = - var annotation = "" - p.bufpos.inc - while true: - case p.buf[p.bufpos] - of ']': - p.state = newickTopology - p.bufpos.inc() - break - else: - annotation.add(p.buf[p.bufpos]) - p.bufpos.inc() - # TODO: Call annotation function if Node is annotabale - when typeof(p.currNode) is ReadableAnnotatedNode: - p.currNode.parseAnnotation(annotation) - -proc parseTopology[T](p: var NewickParser[T]) = - # Parse newick tree - case p.buf[p.bufpos] - of '(': - var newNode = new(T) - p.currNode.addChild(newNode) - p.currNode = newNode - p.bufpos.inc() - p.state = newickLabel - of ',': - var newNode = new(T) - p.currNode.parent.addChild(newNode) - p.currNode = newNode - p.bufpos.inc() - p.state = newickLabel - of ')': - p.currNode = p.currNode.parent - p.bufpos.inc() - p.state = newickLabel - of ';': - if p.currNode == p.root: - p.bufpos.inc() - p.state = newickEnd - else: - p.raiseError("Mismatched parentheses") - else: - p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - -proc parseStart(p: var NewickParser) = - # Parse beginning of newick file - while true: - case p.buf[p.bufpos] - of '(': - p.state = newickTopology - break - of newickWhitespace: - p.parseWhitespace() - of '[': - p.parseBracket() - # if p.buf[p.bufpos+1] == '&': - # case p.buf[p.bufpos+2] - # of 'r', 'R': - # discard - # of 'u', 'U': - # discard - # else: - # p.bufpos.inc(2) - # p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - # if p.buf[p.bufpos+3] == ']': - # p.bufpos.inc(4) - # else: - # p.bufpos.inc(3) - # p.raiseError("Expected \"]\"") - # else: - # p.parseBracket() - of EndOfFile: - # p.state = newickEOF - # break - p.raiseError("Unexpected end of file. No newick statment found.") - else: - p.state = newickLabel - break - -proc parseNewickStream*(stream: Stream, T: typedesc[TraversableNode]): T = - ## Parse a newick stream - var p = NewickParser[T]() - p.root = new(T) - p.currNode = p.root - p.open(stream) - while true: - case p.state - of newickStart: - p.parseStart() - of newickTopology: - p.parseTopology() - of newickLabel: - # when T is LabeledNode: - p.parseLabel() - # when not T is LabeledNode: - # p.skipLabel() - of newickLength: - p.parseLength() - of newickAnnotation: - p.parseData() - of newickEnd: - break - of newickEOF: - break - p.close() - result = p.root - -proc parseNewickString*(T: typedesc[TraversableNode], str: string): T = - ## Parse a newick string - var ss = newStringStream(str) - result = parseNewickStream(ss, T) - ss.close() - - - \ No newline at end of file +proc parseNewickString*(T: typedesc[TraversableNode], str:string): T = + genericBugWorkAround() + var + root = new(T) + curr = root + let p = peg "newick": + dataChars <- Print - {'[', ']'} + S <- *Space + nComment <- >('[' * *(nComment | dataChars) * ']') + comment <- '[' * >*(nComment | dataChars) * ']' + stop <- ';' + lBrack <- '(': + newChildNode(curr) + rBrack <- ')': + branchTerminated(curr) + comma <- ',': + newSisterNode(curr) + label <- >+(Alnum | '_'): + parseLabel(curr, $1) + length <- ':' * >?(+Digit * ?('.' * +Digit)): + parseLength(curr, $1) + data <- >comment: + parseData(curr, $1) + annotation <- ?data * S * ?label * S * ?data * S * ?length * S * ?data + leaf <- annotation + branchset <- (internal | leaf) * S * *(comma * S * (internal | leaf)) + internal <- S * lBrack * S * ?branchset * S * rBrack * S * annotation + start <- *( Space | comment ) + newick <- start * (internal | leaf) * S * stop * S * !1 + + let r = p.match(str) + if not r.ok: + var msg = &"Unexpected '{str[r.matchMax]}' at position {r.matchMax} of Newick string. Problem may originate before this position." + raise newException(NewickError, msg) + if curr != root: + var msg = "Invalid Newick string." + raise newException(NewickError, msg) + result = root + +proc parseNewickFile*(T: typedesc[TraversableNode], path: string): T = + var str = readFile(path) + result = parseNewickString(T, str) + + +# ################################################### +# # Testing + +type + Nd* = ref object + parent*: Nd + children*: seq[Nd] + label*: string + length*: float + data*: string + +proc addChild*(parent, child: Nd) = + ## A bug in Nim currently requires that each type matching that is + ## a TraversableNode must have an addChild proc written for it. + ## This will no longer be necesary when the bug is fixed + ## https://github.com/nim-lang/Nim/issues/22723 + # TODO: Make this a concept once that works + parent.children.add(child) + child.parent = parent + +proc parseNewickData*(n: Nd, data: string) = + n.data = data + +proc writeNewickData*(n: Nd): string = + n.data + +# This works +var t = parseNewickString(Nd, "[[Test]]((([Test]f:1.0[Test[Test]],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") +echo t.ascii \ No newline at end of file diff --git a/src/phylogeni/newickParser2.nim b/src/phylogeni/newickParser2.nim deleted file mode 100644 index 1fca0a8..0000000 --- a/src/phylogeni/newickParser2.nim +++ /dev/null @@ -1,138 +0,0 @@ -import npeg -import ./concepts -import std/[strutils, strformat] - -type - NewickError* = object of IOError - -proc newChildNode[T](curr: var T) = - mixin addChild - var newNode = new(T) - curr.addChild(newNode) - curr = newNode - -proc newSisterNode[T](curr: var T) = - mixin addChild - var newNode = new(T) - curr.parent.addChild(newNode) - curr = newNode - -proc branchTerminated[T](curr: var T) = - curr = curr.parent - -proc parseLabel[T](curr: var T, label: string) = - when curr is LabeledNode: - curr.label = label - -proc parseLength[T](curr: var T, length: string) = - # TODO: Handle errors parsing int and float - when curr is LengthNode: - if length.len > 0: - when curr.length is int: - curr.length = parseInt(length) - when curr.length is float: - curr.length = parseFloat(length) - -proc parseData[T](curr: var T, data: string) = - when curr is ReadableDataNode: - mixin parseNewickData - parseNewickData(curr, data) - -template genericBugWorkAround() = - # Template definitions as workaround for bug in Nim - # https://github.com/zevv/npeg/issues/68 - # https://github.com/nim-lang/Nim/issues/22740 - template `>`(a: untyped): untyped = discard - template `*`(a: untyped): untyped = discard - template `-`(a: untyped): untyped = discard - template `+`(a: untyped): untyped = discard - # template `?`(a: untyped): untyped = discard - # template `!`(a: untyped): untyped = discard - template `$`(a: untyped): untyped = discard - -proc parseNewickString*(T: typedesc[TraversableNode], str:string): T = - genericBugWorkAround() - var - root = new(T) - curr = root - let p = peg "newick": - dataChars <- Print - {'[', ']'} - S <- *Space - nComment <- >('[' * *(nComment | dataChars) * ']') - comment <- '[' * >*(nComment | dataChars) * ']' - stop <- ';' - lBrack <- '(': - newChildNode(curr) - rBrack <- ')': - branchTerminated(curr) - comma <- ',': - newSisterNode(curr) - label <- >+(Alnum | '_'): - parseLabel(curr, $1) - length <- ':' * >?(+Digit * ?('.' * +Digit)): - parseLength(curr, $1) - data <- >comment: - parseData(curr, $1) - annotation <- ?data * S * ?label * S * ?data * S * ?length * S * ?data - leaf <- annotation - branchset <- (internal | leaf) * S * *(comma * S * (internal | leaf)) - internal <- S * lBrack * S * ?branchset * S * rBrack * S * annotation - start <- *( Space | comment ) - newick <- start * (internal | leaf) * S * stop * S * !1 - - let r = p.match(str) - if not r.ok: - var msg = &"Unexpected '{str[r.matchMax]}' at position {r.matchMax} of Newick string. Problem may originate before this position." - raise newException(NewickError, msg) - if curr != root: - var msg = "Invalid Newick string." - raise newException(NewickError, msg) - result = root - -proc parseNewickFile*(T: typedesc[TraversableNode], path: string): T = - var str = readFile(path) - result = parseNewickString(T, str) - - - -# ################################################### -# # Testing - -# discard parseNewickString("(,,(,));") -# discard parseNewickString("(A,B,(C,D));") -# discard parseNewickString("(A,B,(C,D)E)F;") -# discard parseNewickString("(:0.1,:0.2,(:0.3,:0.4):0.5);") -# discard parseNewickString("(:0.1,:0.2,(:0.3,:0.4):0.5):0.0;") -# discard parseNewickString("(A:0.1,B:0.2,(C:0.3,D:0.4):0.5);") -# discard parseNewickString("(A:0.1,B:0.2,(C:0.3,D:0.4)E:0.5)F;") -# discard parseNewickString("((B:0.2,(C:0.3,D:0.4)E:0.5)F:0.1)A;") -# # # TODO: Make test cases with data annotation -# # # TODO: Make test cases expected to fail - - - -type - Nd* = ref object - parent*: Nd - children*: seq[Nd] - label*: string - length*: float - data*: string - -proc addChild*(parent, child: Nd) = - ## A bug in Nim currently requires that each type matching that is - ## a TraversableNode must have an addChild proc written for it. - ## This will no longer be necesary when the bug is fixed - ## https://github.com/nim-lang/Nim/issues/22723 - # TODO: Make this a concept once that works - parent.children.add(child) - child.parent = parent - -proc parseNewickData*(n: Nd, data: string) = - n.data = data - -proc writeNewickData*(n: Nd): string = - n.data - -var t = parseNewickString(Nd, "[[Test]]((([Test]f:1.0[Test[Test]],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") -echo t.ascii \ No newline at end of file diff --git a/src/phylogeni/newickParserOld.nim b/src/phylogeni/newickParserOld.nim new file mode 100644 index 0000000..6705f95 --- /dev/null +++ b/src/phylogeni/newickParserOld.nim @@ -0,0 +1,284 @@ +#TODO: Should rewrite this using a parser library, it has gotten too complex + +import ./concepts, ./traverse +import std/[streams, lexbase, strformat, strutils] + +type + NewickError* = object of IOError + + NewickState = enum + newickStart, newickTopology, newickLabel, newickLength, newickAnnotation, + newickEnd, newickEOF + # TODO: This might be a better way to track state in order to raise errors if + # a newick string doesn't have any parentheses. Low priority given how + # unlikely that is. + # newickStart, newickStartLabel, newickStartLength, newickStartTopology, + # newickTopology, newickLabel, newickLength, newickEnd, newickEOF + + NewickParser[T: TraversableNode] = object of BaseLexer + root: T + currNode: T + token: string + state: NewickState + annotationState: bool # False if an annotation has already been parsed + +const newickWhitespace = {' ', '\t', '\c', '\l'} + +proc raiseError(p: NewickParser, msg: string) = + var + lineNum = $p.lineNumber + colNum = $p.getColNumber(p.bufpos+1) + m = fmt"{msg} at line {lineNum}, column {colNum}" + raise newException(NewickError, m) + +proc parseWhitespace(p: var NewickParser, skip=true) = + while true: + case p.buf[p.bufpos] + of ' ', '\t': + if not skip: p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + of '\c': + if not skip: p.token.add(p.buf[p.bufpos]) + p.bufpos = lexbase.handleCR(p, p.bufpos) + of '\l': # same as \n + if not skip: p.token.add(p.buf[p.bufpos]) + p.bufpos = lexbase.handleLF(p, p.bufpos) + else: + break + +# # proc parseAnnotation(p: var NewickParser[string], annotation: string) = +# # p.currNode.data = annotation + +# # proc parseAnnotation(p: var NewickParser[void], annotation: string) = +# # discard + +proc parseBracket(p: var NewickParser) = + # TODO: handle unexpected end of file and newick statement + mixin parseAnnotation + p.token = "" + p.bufpos.inc() + while true: + case p.buf[p.bufpos] + of ']': + p.bufpos.inc() + break + of newickWhitespace: + p.parseWhitespace(skip=false) + else: + p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + if p.token.startswith('&'): + if p.annotationState: + # p.parseAnnotation(p.token[1..^1]) + p.annotationState = false + +proc parseLength[T](p: var NewickParser[T]) = + #TODO: Determine if length is float or int for nodetype and convert string appropriately + var parseLength = true + while true: + case p.buf[p.bufpos] + of '(', ',', ')', ';': + p.state = newickTopology + break + of newickWhitespace: + p.parseWhitespace() + of '[': + # p.parseBracket() + p.state = newickAnnotation + break + of EndOfFile: + p.raiseError("Unexpected end of stream") + else: + if parseLength: + p.token = "" + while true: + case p.buf[p.bufpos] + of '(', ',', ')', ';', '[', newickWhitespace, EndOfFile: + parseLength = false + break + of '"': + p.raiseError("Unexpected \"") + else: + p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + p.currNode.length = parseFloat(p.token) + parseLength = false + +proc parseLabel(p: var NewickParser) = + # TODO: Write when statement to determine if node has label property + var parseLabel = true + p.annotationState = true + while true: + case p.buf[p.bufpos] + of '(', ',', ')', ';': + p.state = newickTopology + break + of ':': + p.state = newickLength + p.bufpos.inc() + break + of '[': + p.state = newickAnnotation + break + of newickWhitespace: + p.parseWhitespace() + of EndOfFile: + p.raiseError("Unexpected end of stream") + of '"': + # Parse quoted text + if parseLabel: + p.token = "" + p.bufpos.inc() + while true: + case p.buf[p.bufpos] + of '"': + p.bufpos.inc() + break + of newickWhitespace: + p.parseWhitespace(skip=false) + else: + p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + p.currNode.label = p.token + parseLabel = false + else: + p.raiseError("Unexpected \"") + else: + # Parse unquoted text + if parseLabel: + p.token = "" + while true: + case p.buf[p.bufpos] + of '(', ',', ')', ';', ':', '[', ']', newickWhitespace, EndOfFile: + parseLabel = false + break + of '"': + p.raiseError("Unexpected \"") + else: + p.token.add(p.buf[p.bufpos]) + p.bufpos.inc() + p.currNode.label = p.token + parseLabel = false + else: + p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") + +# proc skipLabel(p: var NewickParser) = +# while true: +# case p.buf[p.bufpos] +# of + +proc parseData[T](p: var NewickParser[T]) = + var annotation = "" + p.bufpos.inc + while true: + case p.buf[p.bufpos] + of ']': + p.state = newickTopology + p.bufpos.inc() + break + else: + annotation.add(p.buf[p.bufpos]) + p.bufpos.inc() + # TODO: Call annotation function if Node is annotabale + when typeof(p.currNode) is ReadableAnnotatedNode: + p.currNode.parseAnnotation(annotation) + +proc parseTopology[T](p: var NewickParser[T]) = + # Parse newick tree + case p.buf[p.bufpos] + of '(': + var newNode = new(T) + p.currNode.addChild(newNode) + p.currNode = newNode + p.bufpos.inc() + p.state = newickLabel + of ',': + var newNode = new(T) + p.currNode.parent.addChild(newNode) + p.currNode = newNode + p.bufpos.inc() + p.state = newickLabel + of ')': + p.currNode = p.currNode.parent + p.bufpos.inc() + p.state = newickLabel + of ';': + if p.currNode == p.root: + p.bufpos.inc() + p.state = newickEnd + else: + p.raiseError("Mismatched parentheses") + else: + p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") + +proc parseStart(p: var NewickParser) = + # Parse beginning of newick file + while true: + case p.buf[p.bufpos] + of '(': + p.state = newickTopology + break + of newickWhitespace: + p.parseWhitespace() + of '[': + p.parseBracket() + # if p.buf[p.bufpos+1] == '&': + # case p.buf[p.bufpos+2] + # of 'r', 'R': + # discard + # of 'u', 'U': + # discard + # else: + # p.bufpos.inc(2) + # p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") + # if p.buf[p.bufpos+3] == ']': + # p.bufpos.inc(4) + # else: + # p.bufpos.inc(3) + # p.raiseError("Expected \"]\"") + # else: + # p.parseBracket() + of EndOfFile: + # p.state = newickEOF + # break + p.raiseError("Unexpected end of file. No newick statment found.") + else: + p.state = newickLabel + break + +proc parseNewickStream*(stream: Stream, T: typedesc[TraversableNode]): T = + ## Parse a newick stream + var p = NewickParser[T]() + p.root = new(T) + p.currNode = p.root + p.open(stream) + while true: + case p.state + of newickStart: + p.parseStart() + of newickTopology: + p.parseTopology() + of newickLabel: + # when T is LabeledNode: + p.parseLabel() + # when not T is LabeledNode: + # p.skipLabel() + of newickLength: + p.parseLength() + of newickAnnotation: + p.parseData() + of newickEnd: + break + of newickEOF: + break + p.close() + result = p.root + +proc parseNewickString*(T: typedesc[TraversableNode], str: string): T = + ## Parse a newick string + var ss = newStringStream(str) + result = parseNewickStream(ss, T) + ss.close() + + + \ No newline at end of file diff --git a/src/testNewick.nim b/src/testNewick.nim index d4c566e..d13cc75 100644 --- a/src/testNewick.nim +++ b/src/testNewick.nim @@ -1,55 +1,31 @@ import ./phylogeni -type - Nd* = ref object - parent*: Nd - children*: seq[Nd] - label*: string - length*: float - data*: string - -proc addChild*(parent, child: Nd) = - ## A bug in Nim currently requires that each type matching that is - ## a TraversableNode must have an addChild proc written for it. - ## This will no longer be necesary when the bug is fixed - ## https://github.com/nim-lang/Nim/issues/22723 - # TODO: Make this a concept once that works - parent.children.add(child) - child.parent = parent - -proc parseNewickData*(n: Nd, data: string) = - n.data = data - -proc writeNewickData*(n: Nd): string = - n.data - -# var t = parseNewickString(Nd, "((([Test]f:1.0[Test[Test]],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") -var t = parseNewickString(Nd, "(())") -# echo t.ascii -# for i in t.preorder: -# echo i[] -# t.ladderize() -# echo t.ascii -# prune(t.findNode("f")) -# echo t.ascii -# echo t.writeNewickString() - - - - -# echo t.ascii -# for i in t.preorder: -# echo i.label -# echo "" - -# var c = t.getCoords() -# for i in c.preorder(): -# echo i[] -# echo "" - -# t.ladderize(Descending) -# echo t.ascii -# var c2 = t.getCoords() -# for i in c2.preorder(): -# echo i[] +# type +# Nd* = ref object +# parent*: Nd +# children*: seq[Nd] +# label*: string +# length*: float +# data*: string + +# proc addChild*(parent, child: Nd) = +# ## A bug in Nim currently requires that each type matching that is +# ## a TraversableNode must have an addChild proc written for it. +# ## This will no longer be necesary when the bug is fixed +# ## https://github.com/nim-lang/Nim/issues/22723 +# # TODO: Make this a concept once that works +# parent.children.add(child) +# child.parent = parent + +# proc parseNewickData*(n: Nd, data: string) = +# n.data = data + +# proc writeNewickData*(n: Nd): string = +# n.data + +# A bug with Npeg prevents this from working +var t = parseNewickString(Nd, "((([Test]f:1.0[Test[Test]],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") +# var t = parseNewickString(Nd, "(())") + + From ea2a3621f83922fb7e9c5f1d98f7a1b985bf3030 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Mon, 2 Oct 2023 13:02:09 -0500 Subject: [PATCH 08/13] Cleaning up files and added generic node with parser and writer --- README.md | 2 +- a.nim | 37 ---- b.nim | 4 - src/phylogeni.nim | 17 +- src/phylogeni/newickParser.nim | 60 +++---- src/phylogeni/newickParserOld.nim | 284 ------------------------------ src/phylogeni/newickWriter.nim | 2 - src/phylogeni/nodeTypes.nim | 61 +++++++ src/testNewick.nim | 31 ---- testNewick.nim | 11 ++ 10 files changed, 103 insertions(+), 406 deletions(-) delete mode 100644 a.nim delete mode 100644 b.nim delete mode 100644 src/phylogeni/newickParserOld.nim create mode 100644 src/phylogeni/nodeTypes.nim delete mode 100644 src/testNewick.nim create mode 100644 testNewick.nim diff --git a/README.md b/README.md index 0025633..9cce4d8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # PhylogeNi PhylogeNi is a Nim library with some basic functions for working with phylogenetic trees. -PhylogeNi is a work in progress. Suggestions, contributions, and criticisms are welcome! Breaking changes are likely. +PhylogeNi is a work in progress. Suggestions, criticisms, and especially contributions are welcome! Breaking changes are likely. ## Installation You will need the Nim compiler to be installed on your system. See https://nim-lang.org/ diff --git a/a.nim b/a.nim deleted file mode 100644 index 33f85a0..0000000 --- a/a.nim +++ /dev/null @@ -1,37 +0,0 @@ -import npeg - -# proc parse*(T: typedesc, str:string) = -# template `>`(a: untyped): untyped = discard -# let p = peg "parser": -# parser <- >"test" -# let r = p.match(str) -# # echo r.captures -# parse(int, "test") - -proc parse*[T](o: T, str:string) = - template `>`(a: untyped): untyped = discard - let p = peg "parser": - parser <- >"test" - let r = p.match(str) - # echo r.captures -# parse(1, "test") - - -# proc parse*(T: typedesc, str:string) = -# template `*`(a: untyped): untyped = discard -# template `>`(a: untyped): untyped = discard -# let p = peg "parser": -# elem <- internal -# internal <- '(' * >?elem * ')' -# parser <- >internal -# let r = p.match(str) -# echo r.captures -# parse(int, "(())") - -# proc parse*(str:string) = -# let p = peg "parser": -# elem <- internal -# internal <- '(' * ?elem * ')' -# parser <- internal -# let r = p.match(str) -# parse(str) \ No newline at end of file diff --git a/b.nim b/b.nim deleted file mode 100644 index c45b550..0000000 --- a/b.nim +++ /dev/null @@ -1,4 +0,0 @@ -import ./a - -# parse(int, "test") -parse(1, "test") \ No newline at end of file diff --git a/src/phylogeni.nim b/src/phylogeni.nim index ecdc387..4987586 100644 --- a/src/phylogeni.nim +++ b/src/phylogeni.nim @@ -4,6 +4,7 @@ import ./phylogeni/[ manipulate, newickParser, newickWriter, + nodeTypes, traverse] export @@ -12,12 +13,14 @@ export manipulate, newickParser, newickWriter, + nodeTypes, traverse -# include ./phylogeni/[ -# concepts, -# coordinates, -# manipulate, -# newickParser, -# newickWriter, -# traverse] \ No newline at end of file +## ========= +## PhylogeNi +## ========= +## +## PhylogeNi is a Nim library for working with phylogenetic trees. +## + +# runnableExamples: \ No newline at end of file diff --git a/src/phylogeni/newickParser.nim b/src/phylogeni/newickParser.nim index c71af12..0a44682 100644 --- a/src/phylogeni/newickParser.nim +++ b/src/phylogeni/newickParser.nim @@ -1,5 +1,6 @@ import npeg import ./concepts +import ./nodeTypes import std/[strutils, strformat] type @@ -39,18 +40,19 @@ proc parseData[T](curr: var T, data: string) = parseNewickData(curr, data) template genericBugWorkAround() = - # Template definitions as workaround for bug in Nim + # Workaround for bug in Nim # https://github.com/zevv/npeg/issues/68 # https://github.com/nim-lang/Nim/issues/22740 template `>`(a: untyped): untyped = discard template `*`(a: untyped): untyped = discard template `-`(a: untyped): untyped = discard template `+`(a: untyped): untyped = discard - # template `?`(a: untyped): untyped = discard - # template `!`(a: untyped): untyped = discard - template `$`(a: untyped): untyped = discard - -proc parseNewickString*(T: typedesc[TraversableNode], str:string): T = + +proc parseNewickString*(str: string, T: typedesc[TraversableNode] = DataNode[void]): T = + # TODO: Better error messages + # - empty string + # - missing ';' + # - int/float parsing genericBugWorkAround() var root = new(T) @@ -68,11 +70,14 @@ proc parseNewickString*(T: typedesc[TraversableNode], str:string): T = comma <- ',': newSisterNode(curr) label <- >+(Alnum | '_'): - parseLabel(curr, $1) + # parseLabel(curr, $1) # Can't use $ operator right now due to bug https://github.com/zevv/npeg/issues/68 + parseLabel(curr, capture[1].s) length <- ':' * >?(+Digit * ?('.' * +Digit)): - parseLength(curr, $1) + # parseLength(curr, $1) # Can't use $ operator right now due to bug https://github.com/zevv/npeg/issues/68 + parseLength(curr, capture[1].s) data <- >comment: - parseData(curr, $1) + # parseData(curr, $1) # Can't use $ operator right now due to bug https://github.com/zevv/npeg/issues/68 + parseData(curr, capture[1].s) annotation <- ?data * S * ?label * S * ?data * S * ?length * S * ?data leaf <- annotation branchset <- (internal | leaf) * S * *(comma * S * (internal | leaf)) @@ -89,37 +94,12 @@ proc parseNewickString*(T: typedesc[TraversableNode], str:string): T = raise newException(NewickError, msg) result = root -proc parseNewickFile*(T: typedesc[TraversableNode], path: string): T = +proc parseNewickFile*(path: string, T: typedesc[TraversableNode] = DataNode[void]): T = var str = readFile(path) result = parseNewickString(T, str) - -# ################################################### -# # Testing - -type - Nd* = ref object - parent*: Nd - children*: seq[Nd] - label*: string - length*: float - data*: string - -proc addChild*(parent, child: Nd) = - ## A bug in Nim currently requires that each type matching that is - ## a TraversableNode must have an addChild proc written for it. - ## This will no longer be necesary when the bug is fixed - ## https://github.com/nim-lang/Nim/issues/22723 - # TODO: Make this a concept once that works - parent.children.add(child) - child.parent = parent - -proc parseNewickData*(n: Nd, data: string) = - n.data = data - -proc writeNewickData*(n: Nd): string = - n.data - -# This works -var t = parseNewickString(Nd, "[[Test]]((([Test]f:1.0[Test[Test]],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") -echo t.ascii \ No newline at end of file +proc genericBugWorkaround() = + # Needed to work around bug in Nim + # https://github.com/zevv/npeg/issues/68 + # https://github.com/nim-lang/Nim/issues/22740 + discard parseNewickString(";", DataNode[void]) diff --git a/src/phylogeni/newickParserOld.nim b/src/phylogeni/newickParserOld.nim deleted file mode 100644 index 6705f95..0000000 --- a/src/phylogeni/newickParserOld.nim +++ /dev/null @@ -1,284 +0,0 @@ -#TODO: Should rewrite this using a parser library, it has gotten too complex - -import ./concepts, ./traverse -import std/[streams, lexbase, strformat, strutils] - -type - NewickError* = object of IOError - - NewickState = enum - newickStart, newickTopology, newickLabel, newickLength, newickAnnotation, - newickEnd, newickEOF - # TODO: This might be a better way to track state in order to raise errors if - # a newick string doesn't have any parentheses. Low priority given how - # unlikely that is. - # newickStart, newickStartLabel, newickStartLength, newickStartTopology, - # newickTopology, newickLabel, newickLength, newickEnd, newickEOF - - NewickParser[T: TraversableNode] = object of BaseLexer - root: T - currNode: T - token: string - state: NewickState - annotationState: bool # False if an annotation has already been parsed - -const newickWhitespace = {' ', '\t', '\c', '\l'} - -proc raiseError(p: NewickParser, msg: string) = - var - lineNum = $p.lineNumber - colNum = $p.getColNumber(p.bufpos+1) - m = fmt"{msg} at line {lineNum}, column {colNum}" - raise newException(NewickError, m) - -proc parseWhitespace(p: var NewickParser, skip=true) = - while true: - case p.buf[p.bufpos] - of ' ', '\t': - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - of '\c': - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos = lexbase.handleCR(p, p.bufpos) - of '\l': # same as \n - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos = lexbase.handleLF(p, p.bufpos) - else: - break - -# # proc parseAnnotation(p: var NewickParser[string], annotation: string) = -# # p.currNode.data = annotation - -# # proc parseAnnotation(p: var NewickParser[void], annotation: string) = -# # discard - -proc parseBracket(p: var NewickParser) = - # TODO: handle unexpected end of file and newick statement - mixin parseAnnotation - p.token = "" - p.bufpos.inc() - while true: - case p.buf[p.bufpos] - of ']': - p.bufpos.inc() - break - of newickWhitespace: - p.parseWhitespace(skip=false) - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - if p.token.startswith('&'): - if p.annotationState: - # p.parseAnnotation(p.token[1..^1]) - p.annotationState = false - -proc parseLength[T](p: var NewickParser[T]) = - #TODO: Determine if length is float or int for nodetype and convert string appropriately - var parseLength = true - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';': - p.state = newickTopology - break - of newickWhitespace: - p.parseWhitespace() - of '[': - # p.parseBracket() - p.state = newickAnnotation - break - of EndOfFile: - p.raiseError("Unexpected end of stream") - else: - if parseLength: - p.token = "" - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';', '[', newickWhitespace, EndOfFile: - parseLength = false - break - of '"': - p.raiseError("Unexpected \"") - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.length = parseFloat(p.token) - parseLength = false - -proc parseLabel(p: var NewickParser) = - # TODO: Write when statement to determine if node has label property - var parseLabel = true - p.annotationState = true - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';': - p.state = newickTopology - break - of ':': - p.state = newickLength - p.bufpos.inc() - break - of '[': - p.state = newickAnnotation - break - of newickWhitespace: - p.parseWhitespace() - of EndOfFile: - p.raiseError("Unexpected end of stream") - of '"': - # Parse quoted text - if parseLabel: - p.token = "" - p.bufpos.inc() - while true: - case p.buf[p.bufpos] - of '"': - p.bufpos.inc() - break - of newickWhitespace: - p.parseWhitespace(skip=false) - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.label = p.token - parseLabel = false - else: - p.raiseError("Unexpected \"") - else: - # Parse unquoted text - if parseLabel: - p.token = "" - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';', ':', '[', ']', newickWhitespace, EndOfFile: - parseLabel = false - break - of '"': - p.raiseError("Unexpected \"") - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.label = p.token - parseLabel = false - else: - p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - -# proc skipLabel(p: var NewickParser) = -# while true: -# case p.buf[p.bufpos] -# of - -proc parseData[T](p: var NewickParser[T]) = - var annotation = "" - p.bufpos.inc - while true: - case p.buf[p.bufpos] - of ']': - p.state = newickTopology - p.bufpos.inc() - break - else: - annotation.add(p.buf[p.bufpos]) - p.bufpos.inc() - # TODO: Call annotation function if Node is annotabale - when typeof(p.currNode) is ReadableAnnotatedNode: - p.currNode.parseAnnotation(annotation) - -proc parseTopology[T](p: var NewickParser[T]) = - # Parse newick tree - case p.buf[p.bufpos] - of '(': - var newNode = new(T) - p.currNode.addChild(newNode) - p.currNode = newNode - p.bufpos.inc() - p.state = newickLabel - of ',': - var newNode = new(T) - p.currNode.parent.addChild(newNode) - p.currNode = newNode - p.bufpos.inc() - p.state = newickLabel - of ')': - p.currNode = p.currNode.parent - p.bufpos.inc() - p.state = newickLabel - of ';': - if p.currNode == p.root: - p.bufpos.inc() - p.state = newickEnd - else: - p.raiseError("Mismatched parentheses") - else: - p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - -proc parseStart(p: var NewickParser) = - # Parse beginning of newick file - while true: - case p.buf[p.bufpos] - of '(': - p.state = newickTopology - break - of newickWhitespace: - p.parseWhitespace() - of '[': - p.parseBracket() - # if p.buf[p.bufpos+1] == '&': - # case p.buf[p.bufpos+2] - # of 'r', 'R': - # discard - # of 'u', 'U': - # discard - # else: - # p.bufpos.inc(2) - # p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - # if p.buf[p.bufpos+3] == ']': - # p.bufpos.inc(4) - # else: - # p.bufpos.inc(3) - # p.raiseError("Expected \"]\"") - # else: - # p.parseBracket() - of EndOfFile: - # p.state = newickEOF - # break - p.raiseError("Unexpected end of file. No newick statment found.") - else: - p.state = newickLabel - break - -proc parseNewickStream*(stream: Stream, T: typedesc[TraversableNode]): T = - ## Parse a newick stream - var p = NewickParser[T]() - p.root = new(T) - p.currNode = p.root - p.open(stream) - while true: - case p.state - of newickStart: - p.parseStart() - of newickTopology: - p.parseTopology() - of newickLabel: - # when T is LabeledNode: - p.parseLabel() - # when not T is LabeledNode: - # p.skipLabel() - of newickLength: - p.parseLength() - of newickAnnotation: - p.parseData() - of newickEnd: - break - of newickEOF: - break - p.close() - result = p.root - -proc parseNewickString*(T: typedesc[TraversableNode], str: string): T = - ## Parse a newick string - var ss = newStringStream(str) - result = parseNewickStream(ss, T) - ss.close() - - - \ No newline at end of file diff --git a/src/phylogeni/newickWriter.nim b/src/phylogeni/newickWriter.nim index b96041e..cf198f8 100644 --- a/src/phylogeni/newickWriter.nim +++ b/src/phylogeni/newickWriter.nim @@ -9,9 +9,7 @@ func writeAnnotations(node: TraversableNode, str: var string, data: bool) = when typeof(node) is WritableDataNode: mixin writeNewickData if data: - str.add('[') str.add(node.writeNewickData) - str.add(']') func writeNewickString*(root: TraversableNode, data=true): string = ## Write newick string for Node object diff --git a/src/phylogeni/nodeTypes.nim b/src/phylogeni/nodeTypes.nim new file mode 100644 index 0000000..5ddc878 --- /dev/null +++ b/src/phylogeni/nodeTypes.nim @@ -0,0 +1,61 @@ +import tables +export tables # TODO: How to avoid needing this +import strformat +import npeg +import ./concepts + +type + DataNode*[T] = ref object + parent*: DataNode[T] + children*: seq[DataNode[T]] + label*: string + length*: float + data*: T + + +proc addChild*[T](parent, child: DataNode[T]) = + ## A bug in Nim currently requires that each type matching + ## a TraversableNode must have an addChild proc written for it. + ## https://github.com/nim-lang/Nim/issues/22723 + parent.children.add(child) + child.parent = parent + + +# Void Data +proc parseNewickData*[T](n: DataNode[void], data: string) = + discard + +proc writeNewickData*[T](n: DataNode[void]): string = + result = "" + +# String Data +proc parseNewickData*[T](n: DataNode[string], data: string) = + n.data = data + +proc writeNewickData*[T](n: DataNode[string]): string = + n.data + +# NHX Data +type + NHXData* = OrderedTable[string, string] + +# TODO: Make object variant to use as value for table and modify parser to +# recognize and assign + +proc parseNewickData*(n: DataNode[NHXData], data: string) = + var node = n + let p = peg "parser": + val <- *(Print - {'[', ']'}) + key <- *(Alnum | '_') + pair <- ':' * >key * '=' * >val: + node.data[$1] = $2 + pairs <- ?(pair * *(',' * pair)) + parser <- "[&&NHX" * pairs * ']' + let r = p.match(data) + assert r.ok + +proc writeNewickData*(n: DataNode[NHXData]): string = + result.add("[&&NHX") + for k, v in n.data.pairs: + result.add(fmt":{k}={v}") + result.add(']') \ No newline at end of file diff --git a/src/testNewick.nim b/src/testNewick.nim deleted file mode 100644 index d13cc75..0000000 --- a/src/testNewick.nim +++ /dev/null @@ -1,31 +0,0 @@ -import ./phylogeni - -# type -# Nd* = ref object -# parent*: Nd -# children*: seq[Nd] -# label*: string -# length*: float -# data*: string - -# proc addChild*(parent, child: Nd) = -# ## A bug in Nim currently requires that each type matching that is -# ## a TraversableNode must have an addChild proc written for it. -# ## This will no longer be necesary when the bug is fixed -# ## https://github.com/nim-lang/Nim/issues/22723 -# # TODO: Make this a concept once that works -# parent.children.add(child) -# child.parent = parent - -# proc parseNewickData*(n: Nd, data: string) = -# n.data = data - -# proc writeNewickData*(n: Nd): string = -# n.data - -# A bug with Npeg prevents this from working -var t = parseNewickString(Nd, "((([Test]f:1.0[Test[Test]],g:1.0[Test])e:1.0[Test],d:1.0[Test])c:1.0[Test],b:1.0[Test])a:1.0[Test];") -# var t = parseNewickString(Nd, "(())") - - - diff --git a/testNewick.nim b/testNewick.nim new file mode 100644 index 0000000..27f3806 --- /dev/null +++ b/testNewick.nim @@ -0,0 +1,11 @@ +import ./src/phylogeni + +var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;") +echo t.ascii +t.ladderize(Descending) +echo t.ascii + +var t2 = parseNewickString("(b:1.0[&&NHX:key=b],(d:1.0[&&NHX:key=d],(f:1.0[&&NHX:key=f],g:1.0[&&NHX:key=g])e:1.0[&&NHX:key=e])c:1.0[&&NHX:key=c])a:1.0[&&NHX:key=a];", DataNode[NHXData]) +echo t2.ascii +for i in t2.preorder: + echo i.data["key"] From 76fe8cecc8d489211d6fc5d5ad9f618d1c53a6b6 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Mon, 2 Oct 2023 16:18:39 -0500 Subject: [PATCH 09/13] Fixed a few things --- .github/workflows/tests.yml | 2 +- src/phylogeni.nim | 11 +-------- src/phylogeni/concepts.nim | 32 ++++++++++++-------------- src/phylogeni/manipulate.nim | 25 +++++++++++++------- src/phylogeni/nodeTypes.nim | 4 ++++ src/phylogeni/traverse.nim | 19 ++++++++++++---- testNewick.nim | 44 ++++++++++++++++++++++++++++-------- 7 files changed, 86 insertions(+), 51 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b140563..480c3bf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: BioSeq Tests CI +name: Phylogeni Tests CI on: push: branches: [ "main" ] diff --git a/src/phylogeni.nim b/src/phylogeni.nim index 4987586..0c21ef1 100644 --- a/src/phylogeni.nim +++ b/src/phylogeni.nim @@ -14,13 +14,4 @@ export newickParser, newickWriter, nodeTypes, - traverse - -## ========= -## PhylogeNi -## ========= -## -## PhylogeNi is a Nim library for working with phylogenetic trees. -## - -# runnableExamples: \ No newline at end of file + traverse \ No newline at end of file diff --git a/src/phylogeni/concepts.nim b/src/phylogeni/concepts.nim index 007945b..ba329f8 100644 --- a/src/phylogeni/concepts.nim +++ b/src/phylogeni/concepts.nim @@ -23,24 +23,20 @@ func isRoot*(node: TraversableNode): bool = else: result = false -func getAncestors*(node: TraversableNode): seq[TraversableNode] = - var curr = node - while true: - if curr.parent != nil: - result.add(curr.parent) - curr = curr.parent - else: - break - -func getMRCA*(a, b: TraversableNode): TraversableNode = +# func getAncestors*(node: TraversableNode): seq[TraversableNode] = +# # TODO: Not working +# var curr = node +# while true: +# if curr.parent != nil: +# result.add(curr.parent) +# curr = curr.parent +# else: +# break + +proc getMRCA*(a, b: TraversableNode): TraversableNode = ## Get the most recent common ancestor of two nodes. - # TODO: I think this could be faster adding the elements of the shorter list to a - # hash set and then checking if the elements of the other list belong to that set - let - aAncestors = a.getAncestors - bAncestors = b.getAncestors - for i in aAncestors: - for j in bAncestors: + for i in a.iterAncestors: + for j in b.iterAncestors: if i == j: return i raise newException(TreeError, "No MRCA shared by nodes") @@ -52,7 +48,7 @@ type n is TraversableNode n.label is string -func findNode*(tree: LabeledNode, str: string): LabeledNode = +func find*(tree: LabeledNode, str: string): LabeledNode = ## Returns first instance of node label matching str. for i in tree.preorder: if i.label == str: diff --git a/src/phylogeni/manipulate.nim b/src/phylogeni/manipulate.nim index 46e7676..1b8099c 100644 --- a/src/phylogeni/manipulate.nim +++ b/src/phylogeni/manipulate.nim @@ -13,14 +13,23 @@ func prune*(node: TraversableNode) = if node.parent == nil: raise newException(TreeError, "Cannot prune root node") var parent = node.parent - parent.children.delete(parent.children.find(node)) - if parent.children.len() == 1: - var child = parent.children[0] - parent.children = child.children - when node is LengthNode: - parent.length += child.length - when node is LabeledNode: - parent.label = child.label + node.parent = nil + case parent.children.len: + of 1: + parent.children.setLen(0) + of 2: + if parent.children.len == 2: + var gparent = parent.parent + parent.children.delete(parent.children.find(node)) + let pos = gparent.children.find(parent) + gparent.children[pos] = parent.children[0] + parent.children[0].parent = gparent + when node is LengthNode: + parent.children[0].length += node.length + gparent = nil + parent.children.setLen(0) + else: + parent.children.delete(parent.children.find(node)) type LadderNode[T] = ref object diff --git a/src/phylogeni/nodeTypes.nim b/src/phylogeni/nodeTypes.nim index 5ddc878..f00f5db 100644 --- a/src/phylogeni/nodeTypes.nim +++ b/src/phylogeni/nodeTypes.nim @@ -22,6 +22,9 @@ proc addChild*[T](parent, child: DataNode[T]) = # Void Data +type + NHNode* = DataNode[void] + proc parseNewickData*[T](n: DataNode[void], data: string) = discard @@ -38,6 +41,7 @@ proc writeNewickData*[T](n: DataNode[string]): string = # NHX Data type NHXData* = OrderedTable[string, string] + NHXNode* = DataNode[NHXData] # TODO: Make object variant to use as value for table and modify parser to # recognize and assign diff --git a/src/phylogeni/traverse.nim b/src/phylogeni/traverse.nim index 5e8241d..59ef9b1 100644 --- a/src/phylogeni/traverse.nim +++ b/src/phylogeni/traverse.nim @@ -2,7 +2,7 @@ import ./concepts import std/algorithm iterator preorder*(root: TraversableNode): TraversableNode = - ## Preorder traverse. + ## Preorder traverse of subtree. var stack = @[root] while stack.len > 0: var node = stack.pop() @@ -10,7 +10,7 @@ iterator preorder*(root: TraversableNode): TraversableNode = yield node iterator postorder*(root: TraversableNode): TraversableNode = - ## Postorder traverse. + ## Postorder traverse of subtree. var preStack = @[root] postStack: seq[TraversableNode] @@ -23,7 +23,7 @@ iterator postorder*(root: TraversableNode): TraversableNode = yield node iterator levelorder*(root: TraversableNode): TraversableNode = - ## Levelorder traverse. + ## Levelorder traverse of subtree. yield root var stack = root.children while stack.len > 0: @@ -33,11 +33,20 @@ iterator levelorder*(root: TraversableNode): TraversableNode = stack.add(node.children) iterator iterleaves*(root: TraversableNode): TraversableNode = - ## Iter over leaves. + ## Iter over leaves of subtree. for i in root.preorder(): if i.is_leaf(): yield i +iterator iterAncestors*(node: TraversableNode): TraversableNode = + var curr = node + while true: + if curr.parent != nil: + yield curr.parent + curr = curr.parent + else: + break + type NewickOrderState* = enum ascendingTree, descendingTree @@ -83,4 +92,4 @@ iterator newickorder*[T: TraversableNode](root: T): NewickOrderNode[T] = stack.add(newNewickOrderNode(child, descendingTree)) stack.add(newNewickOrderNode(child, ascendingTree)) else: - stack.add(newNewickOrderNode(child, ascendingTree)) \ No newline at end of file + stack.add(newNewickOrderNode(child, ascendingTree)) diff --git a/testNewick.nim b/testNewick.nim index 27f3806..b1cbd96 100644 --- a/testNewick.nim +++ b/testNewick.nim @@ -1,11 +1,37 @@ import ./src/phylogeni -var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;") -echo t.ascii -t.ladderize(Descending) -echo t.ascii - -var t2 = parseNewickString("(b:1.0[&&NHX:key=b],(d:1.0[&&NHX:key=d],(f:1.0[&&NHX:key=f],g:1.0[&&NHX:key=g])e:1.0[&&NHX:key=e])c:1.0[&&NHX:key=c])a:1.0[&&NHX:key=a];", DataNode[NHXData]) -echo t2.ascii -for i in t2.preorder: - echo i.data["key"] +block: + var t = parseNewickString("((d:1.0,e:1.0)c:1.0,b:1.0)a:1.0;") + echo t.ascii + t.ladderize() + echo t.ascii + echo t.isRoot + var + e = t.find("e") + echo e.isLeaf + var + f = NHNode(label:"f", length:1.0) + g = NHNode(label:"g", length:1.0) + e.addChild(f) + e.addChild(g) + echo t.ascii + var + d = t.find("d") + c = getMRCA(e, d) + echo c + + prune(d) + echo t.ascii + echo t.writeNewickString() + + + +# block: +# var +# s ="(b:1.0[&&NHX:key=b],(d:1.0[&&NHX:key=d],e:1.0[&&NHX:key=e])c:1.0[&&NHX:key=c])a:1.0[&&NHX:key=a];" +# t = parseNewickString(s, NHXNode) +# echo t.ascii +# for i in t.preorder(): +# echo i.data["key"] +# i.data["length"] = $i.length +# echo t.writeNewickString() \ No newline at end of file From de7f07f2e2cb287cc4b95e3a27d577bb178b8e5e Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Thu, 5 Oct 2023 18:00:46 -0500 Subject: [PATCH 10/13] Added nexus parser --- src/phylogeni.nim | 6 +- src/phylogeni/concepts.nim | 675 +-------------------------------- src/phylogeni/coordinates.nim | 50 +-- src/phylogeni/manipulate.nim | 8 +- src/phylogeni/newickParser.nim | 5 +- src/phylogeni/newickWriter.nim | 13 +- src/phylogeni/nexusParser.nim | 190 ++++++++++ src/phylogeni/nodeTypes.nim | 109 +++++- src/phylogeni/traverse.nim | 70 ++-- testNewick.nim | 102 +++-- 10 files changed, 445 insertions(+), 783 deletions(-) create mode 100644 src/phylogeni/nexusParser.nim diff --git a/src/phylogeni.nim b/src/phylogeni.nim index 0c21ef1..36a95ff 100644 --- a/src/phylogeni.nim +++ b/src/phylogeni.nim @@ -1,17 +1,19 @@ import ./phylogeni/[ concepts, - coordinates, + # coordinates, manipulate, newickParser, newickWriter, + nexusParser, nodeTypes, traverse] export concepts, - coordinates, + # coordinates, manipulate, newickParser, newickWriter, + nexusParser, nodeTypes, traverse \ No newline at end of file diff --git a/src/phylogeni/concepts.nim b/src/phylogeni/concepts.nim index ba329f8..9bdf2b1 100644 --- a/src/phylogeni/concepts.nim +++ b/src/phylogeni/concepts.nim @@ -1,4 +1,3 @@ -# import system import std/[strutils, sequtils] type @@ -23,17 +22,7 @@ func isRoot*(node: TraversableNode): bool = else: result = false -# func getAncestors*(node: TraversableNode): seq[TraversableNode] = -# # TODO: Not working -# var curr = node -# while true: -# if curr.parent != nil: -# result.add(curr.parent) -# curr = curr.parent -# else: -# break - -proc getMRCA*(a, b: TraversableNode): TraversableNode = +proc mrca*(a, b: TraversableNode): TraversableNode = ## Get the most recent common ancestor of two nodes. for i in a.iterAncestors: for j in b.iterAncestors: @@ -41,6 +30,7 @@ proc getMRCA*(a, b: TraversableNode): TraversableNode = return i raise newException(TreeError, "No MRCA shared by nodes") + ############################### # Labeled Node type @@ -111,7 +101,6 @@ func ascii*(node: LabeledNode, char1="-", showInternal=true): string = result = lines.join("\n") - ############################### # Length Node type @@ -135,667 +124,17 @@ func treeHeight*(node: LengthNode): float = result = maxHeight + node.length - ############################### -# +# Data readable from Newick string type ReadableDataNode* = concept n n is TraversableNode n.parseNewickData(string) + +############################### +# Data writable to Newick string type WritableDataNode* = concept n n is TraversableNode - n.writeNewickData is string - -# TODO: would this be redundant? -# TODO: could it improve clarity? -# For use by procs in manipulate module -# type -# MutableNode* = concept n -# n is TraversableNode -# n.parent #TODO: how to confirm if this is mutable -# n.children # TODO: how to confirm that this is mutable - - - - - - - - - - - - - - - - - - - -# TODO: Delete everything below eventually, make sure everythnig was copied somewhere else -# import std/algorithm -# import std/strutils -# import std/sequtils -# import system - -# ############################################################# -# # Iterable Node -# type -# TraversableNode* = concept n, type T -# n.parent is T -# for i in n.children: -# i is T - -# # TODO: This causes an error, seems like a bug, reported https://github.com/nim-lang/Nim/issues/22723 -# # proc addChild(parent, child: TraversableNode) = -# # parent.children.add(child) -# # child.parent = parent - -# func isLeaf*(node: TraversableNode): bool = -# ## Check if node is leaf. -# if node.children.len == 0: -# result = true -# else: -# result = false - -# func isRoot*(node: TraversableNode): bool = -# if node.parent.isNil: -# result = true -# else: -# result = false - -# iterator preorder*(root: TraversableNode): TraversableNode = -# ## Preorder traverse. -# var stack = @[root] -# while stack.len > 0: -# var node = stack.pop() -# stack.add(node.children.reversed()) -# yield node - -# iterator postorder*(root: TraversableNode): TraversableNode = -# ## Postorder traverse. -# var -# preStack = @[root] -# postStack: seq[TraversableNode] -# while preStack.len > 0: -# var node = preStack.pop() -# postStack.add(node) -# preStack.add(node.children) -# while postStack.len > 0: -# var node = postStack.pop() -# yield node - -# iterator levelorder*(root: TraversableNode): TraversableNode = -# ## Levelorder traverse. -# yield root -# var stack = root.children -# while stack.len > 0: -# var node = stack[0] -# stack.delete(0) -# yield node -# stack.add(node.children) - -# iterator iterleaves*(root: TraversableNode): TraversableNode = -# ## Iter over leaves. -# for i in root.preorder(): -# if i.is_leaf(): -# yield i - -# # NewickOrder Iterator -# type -# NewickOrderState* = enum -# ascendingTree, descendingTree - -# NewickOrderNode*[T: TraversableNode] = ref object -# node*: T -# state*: NewickOrderState - -# func newNewickOrderNode[T](node: T, state: NewickOrderState): NewickOrderNode[T] = -# NewickOrderNode[T](node:node, state:state) - -# func children*[T](node: NewickOrderNode[T]): seq[T] = -# node.node.children - -# func parent*[T](node: NewickOrderNode[T]): T = -# node.node.parent - -# func isLeaf*[T](node: NewickOrderNode[T]): bool = -# ## Check if node is leaf. -# node.node.isLeaf - -# func isRoot*[T](node: NewickOrderNode[T]): bool = -# node.node.isRoot - -# proc `$`*[T](node: NewickOrderNode[T]): string = -# $node.node & ", " & $node.state - -# iterator newickorder*[T: TraversableNode](root: T): NewickOrderNode[T] = -# ## Newick order traverse. All internal nodes are visited twice. Leaf nodes are -# ## only visited once. This traverese is a hybrid between preorder and -# ## postorder traverse. It is convenient for writing newick strings and -# ## plotting trees. -# var stack: seq[NewickOrderNode[T]] -# stack.add(newNewickOrderNode(root, descendingTree)) -# stack.add(newNewickOrderNode(root, ascendingTree)) -# while stack.len > 0: -# var node = stack.pop() -# yield node -# if not node.isLeaf: -# if node.state == ascendingTree: -# for child in node.children.reversed: -# if not child.isLeaf: -# stack.add(newNewickOrderNode(child, descendingTree)) -# stack.add(newNewickOrderNode(child, ascendingTree)) -# else: -# stack.add(newNewickOrderNode(child, ascendingTree)) - - -# ################################################################ -# # Length Node -# type -# LengthNode = concept n -# n is TraversableNode -# n.length is SomeNumber - -# type -# ReadableAnnotatedNode = concept n -# n is TraversableNode -# n.parseAnnotation(string) - -# type -# WritableAnnotatedNode = concept n -# n is TraversableNode -# n.writeAnnotation is string - - - -# ################################################################ -# # Labelled Node -# type -# LabelledNode = concept n -# n is TraversableNode -# n.label is string - -# func `$`*(node: LabelledNode): string = -# node.label - -# func get_ascii(node: LabelledNode, char1="-", showInternal=true): tuple[clines: seq[string], mid:int] = -# ## Generates ascii string representation of tree. -# var -# len = 3 -# if node.children.len == 0 or showInternal == true: -# if node.label.len > len: -# len = node.label.len -# var -# pad = strutils.repeat(' ', len) -# pa = strutils.repeat(' ', len-1) -# if node.children.len > 0: -# var -# mids: seq[int] -# results: seq[string] -# for child in node.children: -# var char2: string -# if node.children.len == 1: -# char2 = "-" -# elif child == node.children[0]: -# char2 = "/" -# elif child == node.children[^1]: -# char2 = "\\" -# else: -# char2 = "-" -# var (clines, mid) = get_ascii(child, char2, showInternal) -# mids.add(mid+len(results)) -# results.add(clines) -# var -# lo = mids[0] -# hi = mids[^1] -# last = len(results) -# mid = int((lo+hi)/2) -# prefixes: seq[string] -# prefixes.add(sequtils.repeat(pad, lo+1)) -# if mids.len > 1: -# prefixes.add(sequtils.repeat(pa & "|", hi-lo-1)) -# prefixes.add(sequtils.repeat(pad, last-hi)) -# prefixes[mid] = char1 & strutils.repeat("-", len-2) & prefixes[mid][^1] -# var new_results: seq[string] -# for (p, r) in zip(prefixes, results): -# new_results.add(p&r) -# if showInternal: -# var stem = new_results[mid] -# new_results[mid] = stem[0] & node.label & stem[node.label.len+1..^1] -# result = (new_results, mid) -# else: -# result = (@[char1 & "-" & node.label], 0) - -# func ascii*(node: LabelledNode, char1="-", showInternal=true): string = -# ## Returns ascii string representation of tree. -# var (lines, _) = get_ascii(node, char1, showInternal) -# result = lines.join("\n") - - -# ##################################################### -# # Writing Newick String - -# func writeNewickData(node: TraversableNode, str: var string, annotation: bool) = -# when typeof(node) is LabelledNode: -# str.add(node.label) -# when typeof(node) is LengthNode: -# str.add(':') -# str.add($node.length) -# when typeof(node) is WritableAnnotatedNode: -# if annotation: -# str.add(node.writeAnnotation) - -# func writeNewickString*(root: TraversableNode, annotation=true): string = -# ## Write newick string for Node object -# var str = "" -# for i in root.newickorder(): -# if i.state == ascendingTree: -# if i.node.isLeaf(): -# i.node.writeNewickData(str, annotation) -# if i.node != i.parent.children[^1]: # not the first node in parents children -# str.add(",") -# else: # internal node -# str.add("(") -# else: # descending tree -# str.add(")") -# i.node.writeNewickData(str, annotation) -# if (i.node != root) and (i.node != i.parent.children[^1]): # not last node in parents children -# str.add(",") -# str.add(";") -# result = str - -# ################################################ -# ################################################ -# ################################################ -# ################################################ -# ################################################ -# ################################################ -# ################################################ -# ################################################ -# ################################################ -# ################################################ -# # Parse Newick - -# import std/[streams, lexbase, strformat, strutils] - -# type -# NewickError* = object of IOError - -# NewickState = enum -# newickStart, newickTopology, newickLabel, newickLength, newickAnnotation, -# newickEnd, newickEOF -# # TODO: This might be a better way to track state in order to raise errors if -# # a newick string doesn't have any parentheses. Low priority given how -# # unlikely that is. -# # newickStart, newickStartLabel, newickStartLength, newickStartTopology, -# # newickTopology, newickLabel, newickLength, newickEnd, newickEOF - -# NewickParser[T: TraversableNode] = object of BaseLexer -# root: T -# currNode: T -# token: string -# state: NewickState -# annotationState: bool # False if an annotation has already been parsed - -# const newickWhitespace = {' ', '\t', '\c', '\l'} - -# proc raiseError(p: NewickParser, msg: string) = -# var -# lineNum = $p.lineNumber -# colNum = $p.getColNumber(p.bufpos+1) -# m = fmt"{msg} at line {lineNum}, column {colNum}" -# raise newException(NewickError, m) - -# proc parseWhitespace(p: var NewickParser, skip=true) = -# while true: -# case p.buf[p.bufpos] -# of ' ', '\t': -# if not skip: p.token.add(p.buf[p.bufpos]) -# p.bufpos.inc() -# of '\c': -# if not skip: p.token.add(p.buf[p.bufpos]) -# p.bufpos = lexbase.handleCR(p, p.bufpos) -# of '\l': # same as \n -# if not skip: p.token.add(p.buf[p.bufpos]) -# p.bufpos = lexbase.handleLF(p, p.bufpos) -# else: -# break - -# # # proc parseAnnotation(p: var NewickParser[string], annotation: string) = -# # # p.currNode.data = annotation - -# # # proc parseAnnotation(p: var NewickParser[void], annotation: string) = -# # # discard - -# proc parseBracket(p: var NewickParser, showComments=false) = -# # TODO: handle unexpected end of file and newick statement -# mixin parseAnnotation -# p.token = "" -# p.bufpos.inc() -# while true: -# case p.buf[p.bufpos] -# of ']': -# p.bufpos.inc() -# break -# of newickWhitespace: -# p.parseWhitespace(skip=false) -# else: -# p.token.add(p.buf[p.bufpos]) -# p.bufpos.inc() -# if p.token.startswith('&'): -# if p.annotationState: -# # p.parseAnnotation(p.token[1..^1]) -# p.annotationState = false -# else: -# if showComments: -# echo p.token - -# proc parseLength(p: var NewickParser) = -# #TODO: Determine if length is float or int for nodetype and convert string appropriately -# var parseLength = true -# while true: -# case p.buf[p.bufpos] -# of '(', ',', ')', ';': -# p.state = newickTopology -# break -# of newickWhitespace: -# p.parseWhitespace() -# of '[': -# # p.parseBracket() -# p.state = newickAnnotation -# break -# of EndOfFile: -# p.raiseError("Unexpected end of stream") -# else: -# if parseLength: -# p.token = "" -# while true: -# case p.buf[p.bufpos] -# of '(', ',', ')', ';', '[', newickWhitespace, EndOfFile: -# parseLength = false -# break -# of '"': -# p.raiseError("Unexpected \"") -# else: -# p.token.add(p.buf[p.bufpos]) -# p.bufpos.inc() -# p.currNode.length = parseFloat(p.token) -# parseLength = false - -# proc parseLabel(p: var NewickParser) = -# # TODO: Write when statement to determine if node has label property -# var parseLabel = true -# p.annotationState = true -# while true: -# case p.buf[p.bufpos] -# of '(', ',', ')', ';': -# p.state = newickTopology -# break -# of ':': -# p.state = newickLength -# p.bufpos.inc() -# break -# of '[': -# p.state = newickAnnotation -# break -# # p.parseBracket() -# of newickWhitespace: -# p.parseWhitespace() -# of EndOfFile: -# p.raiseError("Unexpected end of stream") -# of '"': -# # Parse quoted text -# if parseLabel: -# p.token = "" -# p.bufpos.inc() -# while true: -# case p.buf[p.bufpos] -# of '"': -# p.bufpos.inc() -# break -# of newickWhitespace: -# p.parseWhitespace(skip=false) -# else: -# p.token.add(p.buf[p.bufpos]) -# p.bufpos.inc() -# p.currNode.label = p.token -# parseLabel = false -# else: -# p.raiseError("Unexpected \"") -# else: -# # Parse unquoted text -# if parseLabel: -# p.token = "" -# while true: -# case p.buf[p.bufpos] -# of '(', ',', ')', ';', ':', '[', ']', newickWhitespace, EndOfFile: -# parseLabel = false -# break -# of '"': -# p.raiseError("Unexpected \"") -# else: -# p.token.add(p.buf[p.bufpos]) -# p.bufpos.inc() -# p.currNode.label = p.token -# parseLabel = false -# else: -# p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - -# proc parseData(p: var NewickParser) = -# var annotation = "" -# p.bufpos.inc -# while true: -# case p.buf[p.bufpos] -# of ']': -# p.state = newickTopology -# p.bufpos.inc() -# break -# else: -# annotation.add(p.buf[p.bufpos]) -# p.bufpos.inc() -# # TODO: Call annotation function if Node is annotabale -# when typeof(p.currNode) is ReadableAnnotatedNode: -# p.currNode.parseAnnotation(annotation) - -# proc parseTopology(p: var NewickParser, T: typedesc[TraversableNode]) = -# # Parse newick tree -# case p.buf[p.bufpos] -# of '(': -# var newNode = new(T) -# p.currNode.addChild(newNode) -# p.currNode = newNode -# p.bufpos.inc() -# p.state = newickLabel -# of ',': -# var newNode = new(T) -# p.currNode.parent.addChild(newNode) -# p.currNode = newNode -# p.bufpos.inc() -# p.state = newickLabel -# of ')': -# p.currNode = p.currNode.parent -# p.bufpos.inc() -# p.state = newickLabel -# of ';': -# if p.currNode == p.root: -# p.bufpos.inc() -# p.state = newickEnd -# else: -# p.raiseError("Mismatched parentheses") -# else: -# p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - -# proc parseStart(p: var NewickParser) = -# # Parse beginning of newick file -# while true: -# case p.buf[p.bufpos] -# of '(': -# p.state = newickTopology -# break -# of newickWhitespace: -# p.parseWhitespace() -# of '[': -# if p.buf[p.bufpos+1] == '&': -# case p.buf[p.bufpos+2] -# of 'r', 'R': -# discard -# of 'u', 'U': -# discard -# else: -# p.bufpos.inc(2) -# p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") -# if p.buf[p.bufpos+3] == ']': -# p.bufpos.inc(4) -# else: -# p.bufpos.inc(3) -# p.raiseError("Expected \"]\"") -# else: -# p.parseBracket() -# of EndOfFile: -# # p.state = newickEOF -# # break -# p.raiseError("Unexpected end of file. No newick statment found.") -# else: -# p.state = newickLabel -# break - -# proc parseTree(p: var NewickParser, T: typedesc[TraversableNode]) = -# p.parseWhitespace() -# while true: -# case p.state -# of newickStart: -# p.parseStart() -# of newickTopology: -# p.parseTopology(T) -# of newickLabel: -# p.parseLabel() -# of newickLength: -# p.parseLength() -# of newickAnnotation: -# p.parseData() -# of newickEnd: -# break -# of newickEOF: -# break - -# proc parseNewickStream*(stream: Stream, T: typedesc[TraversableNode]): T = -# ## Parse a newick stream -# var -# p = NewickParser[T]() -# p.root = new(T) -# p.currNode = p.root -# p.open(stream) -# p.parseTree(T) -# p.close() -# result = p.root - -# proc parseNewickString*(str: string, T: typedesc[TraversableNode]): T = -# ## Parse a newick string -# var ss = newStringStream(str) -# result = parseNewickStream(ss, T) -# ss.close() - - -# ############################################# -# # Drawing - -# type -# CoordNode*[T] = ref object -# parent: CoordNode[T] -# children: seq[CoordNode[T]] -# x: float # Horizontal position of node, equivalent to node height -# y: float # Vertical position of node -# node: T - -# proc newCoordNode[T: TraversableNode](node: T): CoordNode[T] = -# result = CoordNode[T](node: new(T)) -# result.node[] = node[] - -# proc addChild[T: TraversableNode](parent, child: CoordNode[T]) = -# parent.children.add(child) -# child.parent = parent -# parent.node.children.add(child.node) -# child.node.parent = parent.node.parent -# # parent.node.addChild(child.node) # TODO: Use this when the proc for TraversableNode concept works - -# proc getCoords*[T: LengthNode](root: T, branchLengthScaling=1.0, branchSep=1.0): CoordNode[T] = -# ## Return coordinates for a typical rectangular or slanted phylogeny -# assert branchLengthScaling > 0 -# assert branchSep > 0 -# var -# leafY = 0.0 -# currNode = CoordNode[T](node: new(T)) # Placeholder, is parent to root node of new tree -# for i in root.newickorder: -# case i.state -# of ascendingTree: -# var newNode = newCoordNode(i.node) -# currNode.addChild(newNode) -# newNode.x = currNode.x + (i.node.length * branchLengthScaling) -# if i.node.isLeaf: -# newNode.y = leafY -# leafY += branchSep -# else: -# currNode = newNode -# of descendingTree: -# let -# lo = currNode.children[0].y -# up = currNode.children[^1].y -# currNode.y = (up - lo) / 2 + lo -# currNode = currNode.parent -# result = currNode.children[0] - - -# ############################################# -# # Testing - -# type -# Nd = ref object -# parent: Nd -# children: seq[Nd] -# label: string -# length: float -# data: string - -# proc addChild(parent, child: Nd) = -# # TODO: Make this a concept once that works -# parent.children.add(child) -# child.parent = parent - -# proc writeAnnotation(node: Nd): string = -# result.add('[') -# result.add(node.data) -# result.add(']') - -# proc parseAnnotation(node: Nd, str: string) = -# node.data = str - - - -# var t = parseNewickString("(b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;", Nd) -# echo t.writeNewickString(false) - -# # Bad newick strings -# # TODO: Fix parser to catch these and raise exception with helpful error msg -# # var -# # str = "(B:1.0, [test]C:1.0)A:1.0;" #TODO: Fix error msg -# # str = "(B:1.0,C:[test]1.0)A:1.0;" #TODO: Fix error msg -# # str = "(B:1.0,C:1.0:[test])A:1.0;" #TODO: Fix error msg -# # str = "B:1.0,C:1.0:[test])A:1.0;" #TODO: Fix error msg -# # t = parseNewickString(str, Nd) - -# echo t.ascii - -# var c = t.getCoords() -# for i in c.preorder: -# echo i[] - - - - - - -# # + n.writeNewickData is string \ No newline at end of file diff --git a/src/phylogeni/coordinates.nim b/src/phylogeni/coordinates.nim index 27c7d28..8709e31 100644 --- a/src/phylogeni/coordinates.nim +++ b/src/phylogeni/coordinates.nim @@ -34,28 +34,28 @@ proc addChild[T: TraversableNode](parent, child: CoordNode[T]) = child.node.parent = parent.node.parent # parent.node.addChild(child.node) # TODO: Use this when the proc for TraversableNode concept works -proc getCoords*[T: LengthNode](root: T, branchLengthScaling=1.0, branchSep=1.0): CoordNode[T] = - ## Return coordinates for a typical rectangular or slanted phylogeny - assert branchLengthScaling > 0 - assert branchSep > 0 - var - leafY = 0.0 - currNode = CoordNode[T](node: new(T)) # Placeholder, is parent to root node of new tree - for i in root.newickorder: - case i.state - of ascendingTree: - var newNode = newCoordNode(i.node) - currNode.addChild(newNode) - newNode.x = currNode.x + (i.node.length * branchLengthScaling) - if i.node.isLeaf: - newNode.y = leafY - leafY += branchSep - else: - currNode = newNode - of descendingTree: - let - lo = currNode.children[0].y - up = currNode.children[^1].y - currNode.y = (up - lo) / 2 + lo - currNode = currNode.parent - result = currNode.children[0] \ No newline at end of file +# proc getCoords*[T: LengthNode](root: T, branchLengthScaling=1.0, branchSep=1.0): CoordNode[T] = +# ## Return coordinates for a typical rectangular or slanted phylogeny +# assert branchLengthScaling > 0 +# assert branchSep > 0 +# var +# leafY = 0.0 +# currNode = CoordNode[T](node: new(T)) # Placeholder, is parent to root node of new tree +# for i in root.newickorder: +# case i.state +# of ascendingTree: +# var newNode = newCoordNode(i.node) +# currNode.addChild(newNode) +# newNode.x = currNode.x + (i.node.length * branchLengthScaling) +# if i.node.isLeaf: +# newNode.y = leafY +# leafY += branchSep +# else: +# currNode = newNode +# of descendingTree: +# let +# lo = currNode.children[0].y +# up = currNode.children[^1].y +# currNode.y = (up - lo) / 2 + lo +# currNode = currNode.parent +# result = currNode.children[0] \ No newline at end of file diff --git a/src/phylogeni/manipulate.nim b/src/phylogeni/manipulate.nim index 1b8099c..a0170e9 100644 --- a/src/phylogeni/manipulate.nim +++ b/src/phylogeni/manipulate.nim @@ -40,13 +40,11 @@ type proc ladderize*[T: TraversableNode](root: T, order: SortOrder = Ascending) = ## Ladderize subtree. - # Should benchmark this against hash approach, first figure out implementing hashes with concept - # Could probably come up with more efficient way to sort using the current approach + # Could probably come up with more efficient way to sort # Getting the index of the sorted children rather than the children would be simpler - # and there wouldn't have to be a node attribute for LadderNode var currNode = LadderNode[T]() - for i in root.newickorder: - case i.state + for i in root.allorder: + case i.direction of ascendingTree: var newNode = LadderNode[T](parent:currNode, node:i.node) currNode.children.add(newNode) diff --git a/src/phylogeni/newickParser.nim b/src/phylogeni/newickParser.nim index 0a44682..2d265d4 100644 --- a/src/phylogeni/newickParser.nim +++ b/src/phylogeni/newickParser.nim @@ -58,8 +58,9 @@ proc parseNewickString*(str: string, T: typedesc[TraversableNode] = DataNode[voi root = new(T) curr = root let p = peg "newick": - dataChars <- Print - {'[', ']'} S <- *Space + sciNot <- +Digit * ?('.' * +Digit) * ?(i"e" * ?'-' * *Digit) + dataChars <- Print - {'[', ']'} nComment <- >('[' * *(nComment | dataChars) * ']') comment <- '[' * >*(nComment | dataChars) * ']' stop <- ';' @@ -72,7 +73,7 @@ proc parseNewickString*(str: string, T: typedesc[TraversableNode] = DataNode[voi label <- >+(Alnum | '_'): # parseLabel(curr, $1) # Can't use $ operator right now due to bug https://github.com/zevv/npeg/issues/68 parseLabel(curr, capture[1].s) - length <- ':' * >?(+Digit * ?('.' * +Digit)): + length <- ':' * >?sciNot: # parseLength(curr, $1) # Can't use $ operator right now due to bug https://github.com/zevv/npeg/issues/68 parseLength(curr, capture[1].s) data <- >comment: diff --git a/src/phylogeni/newickWriter.nim b/src/phylogeni/newickWriter.nim index cf198f8..2845e58 100644 --- a/src/phylogeni/newickWriter.nim +++ b/src/phylogeni/newickWriter.nim @@ -11,21 +11,22 @@ func writeAnnotations(node: TraversableNode, str: var string, data: bool) = if data: str.add(node.writeNewickData) -func writeNewickString*(root: TraversableNode, data=true): string = +proc writeNewickString*(root: TraversableNode, data=true): string = ## Write newick string for Node object var str = "" - for i in root.newickorder(): - if i.state == ascendingTree: + for i in root.allorder(): + case i.direction + of ascendingTree: if i.node.isLeaf(): i.node.writeAnnotations(str, data) - if i.node != i.parent.children[^1]: # not the first node in parents children + if i.node != i.node.parent.children[^1]: # not the first node in parents children str.add(",") else: # internal node str.add("(") - else: # descending tree + of descendingTree: str.add(")") i.node.writeAnnotations(str, data) - if (i.node != root) and (i.node != i.parent.children[^1]): # not last node in parents children + if (i.node != root) and (i.node != i.node.parent.children[^1]): # not last node in parents children str.add(",") str.add(";") result = str \ No newline at end of file diff --git a/src/phylogeni/nexusParser.nim b/src/phylogeni/nexusParser.nim new file mode 100644 index 0000000..608caa1 --- /dev/null +++ b/src/phylogeni/nexusParser.nim @@ -0,0 +1,190 @@ +import npeg +import ./concepts +import ./nodeTypes +import ./newickParser +import std/[tables, strformat, strutils] + +type + NexusKind* = enum nexusData, nexusTaxa, nexusTrees, nexusUndefined + + NexusBlock*[T] = object + case kind*: NexusKind + of nexusTrees: + translate*: OrderedTable[string, string] + trees*: seq[tuple[label: string, tree:T]] + of nexusTaxa: + ntaxa*: int + taxa*: seq[string] + of nexusData: + nchar*: int + of nexusUndefined: + blockName*: string + blockString*: string + + Nexus*[T] = object + blocks*: seq[NexusBlock[T]] + + +# TODO: In the future it should be possible to do this instead of the current +# approach: +# type Nexus*[T] = distinct seq[NexusBlock[T]] +# proc len*[T](n: Nexus[T]): int {.borrow.} +# proc `$`*(j: Nexus[T]): string {.borrow.} +# proc `==`*(a, b: Nexus[T]): bool {.borrow.} +# proc add*(j: var Nexus[T], n: int) {.borrow.} +# proc `[]`*(a: Nexus[T], i: int): int = seq[int](a)[i] +# proc join*(a: Nexus[T], s: string): string {.borrow.} + + +proc len*[T](n: Nexus[T]): int = + n.blocks.len + +proc add*[T](n: var Nexus[T], b: NexusBlock[T]) = + n.blocks.add(b) + +proc `[]`*[T](n: Nexus[T], i: int): NexusBlock[T] = + n.blocks[i] + +proc `[]=`*[T](n: var Nexus[T], i:int, b: NexusBlock[T]) = + n.blocks[i] = b + +proc find*[T](n: Nexus[T], b: NexusBlock[T]): int = + n.blocks.fid(b) + +proc delete*[T](n: var Nexus[T], i: int) = + n.blocks.delete(i) + +iterator items*[T](n: Nexus[T]): NexusBlock[T] = + for i in n.blocks.items: + yield i + +proc `$`*[T](n: Nexus[T]): string = + result.add("Nexus:\n") + var cnt = 0 + for i in n: + result.add(&" {cnt}: {i.kind}\n") + cnt += 1 + +template genericBugWorkAround() = + # Workaround for bug in Nim + # https://github.com/zevv/npeg/issues/68 + # https://github.com/nim-lang/Nim/issues/22740 + template `>`(a: untyped): untyped = discard + template `*`(a: untyped): untyped = discard + template `-`(a: untyped): untyped = discard + template `+`(a: untyped): untyped = discard + template `@`(a: untyped): untyped = discard + +proc parseDataBlock[T](nex: var Nexus[T], str: string) = + # TODO: Work in progress + var dataBlock = NexusBlock[T](kind:nexusData) + genericBugWorkAround() + let p = peg "data": + s <- *Space + S <- +Space + nchar <- S * >i"nchar=" * >+Digit + ntax <- S * >i"ntax=" * >+Digit + dimensions <- s * >i"dimensions" * ntax * nchar * s * ';' + datatype <- S * >i"datatype=" * >+Alpha + missing <- S * >i"missing=" * >'?' + gap <- S * >i"gap=" * >'-' + dformat <- s * >i"format" * datatype * missing * gap * s * ';' + sample <- S * >+Alpha * S * >+(Alpha | {'-', '?'}) + matrix <- s * >i"matrix" * +sample * s * ';' + data <- dimensions * dformat * matrix + let r = p.match(str) + nex.add(dataBlock) + +proc parseTaxaBlock[T](nex: var Nexus[T], str: string) = + var taxaBlock = NexusBlock[T](kind:nexusTaxa) + genericBugWorkAround() + let p = peg "taxa": + s <- *Space + S <- +Space + label <- S * >+(Alnum | '_'): + taxaBlock.taxa.add(capture[1].s) + labels <- i"taxlabels" * +label * s * ';' + dimensions <- i"dimensions" * S * i"ntax=" * >*Digit * ';': + taxaBlock.ntaxa = parseInt(capture[1].s) + taxa <- s * dimensions * S * labels + let r = p.match(str) + assert r.ok + nex.add(taxaBlock) + +proc parseTreesBlock[T](nex: var Nexus[T], str: string) = + var treeBlock = NexusBlock[T](kind:nexusTrees) + genericBugWorkAround() + let p = peg "trees": + S <- *Space + label <- *(Alnum | {'_', '.', '-'}) + pair <- S * >?label * S * >label: + treeBlock.translate[capture[1].s] = capture[2].s + paired <- pair * *(S * ',' * pair) + translate <- >i"translate" * paired * @';' + tree <- S * i"tree" * S * >label * S * '=' * >@';': + var t = parseNewickString(capture[2].s, T) + treeBlock.trees.add((capture[1].s, t)) + trees <- S * ?translate * +tree + let r = p.match(str) + assert r.ok + nex.blocks.add(treeBlock) + +proc parseNexusString*(str: string, T: typedesc[TraversableNode] = NexusNode): Nexus[T] = + genericBugWorkAround() + var nex = Nexus[T]() + let p = peg "nexus": + S <- *Space + label <- *(Alnum | {'_', '.', '-'}) + data <- i"data;" * >*(1-i"end;"): + parseDataBlock(nex, capture[1].s) + taxa <- i"taxa;" * >*(1-i"end;"): + parseTaxaBlock(nex, capture[1].s) + trees <- i"trees;" * >*(1-i"end;"): + parseTreesBlock(nex, capture[1].s) + undefined <- >+Alpha * ';' * >*(1 - i"end;"): + echo capture[1].s + echo capture[2].s + # parseUndefinedBlock(nex, capture[1].s) + kind <- (data | taxa | trees | undefined) + nblock <- i"begin" * S * kind * S * i"end;" + nexus <- i"#nexus" * S * nblock * *(S * nblock) * S * !1 + let r = p.match(str) + assert r.ok + result = nex + +proc parseNexusFile*(path: string, T: typedesc[TraversableNode] = NexusNode): Nexus[T] = + var str = readFile(path) + result = parseNexusString(str, T) + + +var str = """ +#NEXUS +Begin TAXA; + Dimensions ntax=4; + TaxLabels SpaceDog SpaceCat SpaceOrc SpaceElf; +End; + +Begin data; +dimensions ntax=5 nchar=54; +format datatype=dna missing=? gap=-; +matrix + Ephedra TTAAGCCATGCATGTCTAAGTATGAACTAATTCCAAACGGTGAAACTGCGGATG + Gnetum TTAAGCCATGCATGTCTATGTACGAACTAATC-AGAACGGTGAAACTGCGGATG + Welwitschia TTAAGCCATGCACGTGTAAGTATGAACTAGTC-GAAACGGTGAAACTGCGGATG + Ginkgo TTAAGCCATGCATGTGTAAGTATGAACTCTTTACAGACTGTGAAACTGCGAATG + Pinus TTAAGCCATGCATGTCTAAGTATGAACTAATTGCAGACTGTGAAACTGCGGATG +; +End; + +BEGIN TREES; + Tree tree1 = (((SpaceDog,SpaceCat),SpaceOrc,SpaceElf)); +END; + +BEGIN PAUP; + Dumb paup commands +END; +""" + + +var n = parseNexusString(str) +echo n \ No newline at end of file diff --git a/src/phylogeni/nodeTypes.nim b/src/phylogeni/nodeTypes.nim index f00f5db..8f6da9a 100644 --- a/src/phylogeni/nodeTypes.nim +++ b/src/phylogeni/nodeTypes.nim @@ -4,6 +4,37 @@ import strformat import npeg import ./concepts +type + NodeDataKind* = enum ndString, ndFloat, ndInt, ndArray + + NodeData* = object + case kind*: NodeDataKind + of ndString: + ndString*: string + of ndFloat: + ndFloat*: float + of ndInt: + ndInt*: int + of ndArray: + ndArray*: seq[NodeData] + +proc `$`*(d: NodeData): string = + case d.kind + of ndString: + result = d.ndString + of ndFloat: + result = $d.ndFloat + of ndInt: + result = $d.ndInt + of ndArray: + result.add('{') + for i, x in d.ndArray: + result.add($x) + if i < d.ndArray.len - 1: + result.add(',') + result.add('}') + +# Generic data node type DataNode*[T] = ref object parent*: DataNode[T] @@ -11,7 +42,6 @@ type label*: string length*: float data*: T - proc addChild*[T](parent, child: DataNode[T]) = ## A bug in Nim currently requires that each type matching @@ -31,6 +61,7 @@ proc parseNewickData*[T](n: DataNode[void], data: string) = proc writeNewickData*[T](n: DataNode[void]): string = result = "" + # String Data proc parseNewickData*[T](n: DataNode[string], data: string) = n.data = data @@ -38,28 +69,84 @@ proc parseNewickData*[T](n: DataNode[string], data: string) = proc writeNewickData*[T](n: DataNode[string]): string = n.data + # NHX Data -type - NHXData* = OrderedTable[string, string] - NHXNode* = DataNode[NHXData] +# TODO: Look into implementing this using distinct table with borrowed procs +# instead of makeing a whole new Node type +type + NHXNode* = ref object + parent*: NHXNode + children*: seq[NHXNode] + label*: string + length*: float + data*: OrderedTable[string, NodeData] -# TODO: Make object variant to use as value for table and modify parser to -# recognize and assign +proc addChild*(parent, child: NHXNode) = + ## A bug in Nim currently requires that each type matching + ## a TraversableNode must have an addChild proc written for it. + ## https://github.com/nim-lang/Nim/issues/22723 + parent.children.add(child) + child.parent = parent -proc parseNewickData*(n: DataNode[NHXData], data: string) = +proc parseNewickData*(n: NHXNode, data: string) = + # TODO: Parse things other than strings into their NodeData variant var node = n let p = peg "parser": val <- *(Print - {'[', ']'}) key <- *(Alnum | '_') pair <- ':' * >key * '=' * >val: - node.data[$1] = $2 + node.data[capture[1].s] = NodeData(kind: ndString, ndString:capture[2].s) pairs <- ?(pair * *(',' * pair)) parser <- "[&&NHX" * pairs * ']' let r = p.match(data) assert r.ok -proc writeNewickData*(n: DataNode[NHXData]): string = +proc writeNewickData*(n: NHXNode): string = result.add("[&&NHX") for k, v in n.data.pairs: - result.add(fmt":{k}={v}") - result.add(']') \ No newline at end of file + result.add(fmt":{k}={$v}") + result.add(']') + + +# Nexus Data +# TODO: Look into implementing this using distinct table with borrowed procs +# instead of makeing a whole new Node type +type + NexusNode* = ref object + parent*: NexusNode + children*: seq[NexusNode] + label*: string + length*: float + data*: OrderedTable[string, NodeData] + +proc addChild*(parent, child: NexusNode) = + ## A bug in Nim currently requires that each type matching + ## a TraversableNode must have an addChild proc written for it. + ## https://github.com/nim-lang/Nim/issues/22723 + parent.children.add(child) + child.parent = parent + +proc parseNewickData*(n: NexusNode, data: string) = +# proc parseNewickData*(data: string) = + # TODO: Parse things other than strings into their NodeData variant + # echo data + var node = n + let p = peg "parser": + value <- *(Alnum | '.') + darray <- '{' * *(Alnum | {'.', ','}) * '}' + key <- *(Alnum | {'_', '%', '-', '.'}) + pair <- >key * '=' * >(darray | value): + node.data[capture[1].s] = NodeData(kind: ndString, ndString:capture[2].s) + pairs <- ?pair * *(',' * pair) + parser <- "[&" * pairs * @']' + let r = p.match(data) + assert r.ok + +proc writeNewickData*(n: NexusNode): string = + result.add("[&") + for k, v in n.data.pairs: + result.add(fmt",{k}={$v}") + result.add(']') + +# var str = "[&height_95%_HPD={350.7986790000001,350.99962300000016},length=0.0,posterior=1.0,height_median=350.96059650000007,height_range={350.73167,350.99962300000016},height=350.9389471000002]" +# parseNewickData(str) diff --git a/src/phylogeni/traverse.nim b/src/phylogeni/traverse.nim index 59ef9b1..6d7e599 100644 --- a/src/phylogeni/traverse.nim +++ b/src/phylogeni/traverse.nim @@ -47,49 +47,43 @@ iterator iterAncestors*(node: TraversableNode): TraversableNode = else: break + +# TODO: Seems that there is a bug in Nim resulting in tuple causing error +# despite the code working. Revert to using tuple at some point to simplify this. type - NewickOrderState* = enum - ascendingTree, descendingTree + AllorderDirection* = enum ascendingTree, descendingTree - NewickOrderNode*[T: TraversableNode] = ref object + Allorder*[T: TraversableNode] = object node*: T - state*: NewickOrderState - -func newNewickOrderNode[T](node: T, state: NewickOrderState): NewickOrderNode[T] = - NewickOrderNode[T](node:node, state:state) - -func children*[T](node: NewickOrderNode[T]): seq[T] = - node.node.children - -func parent*[T](node: NewickOrderNode[T]): T = - node.node.parent + direction*: AllorderDirection -func isLeaf*[T](node: NewickOrderNode[T]): bool = - ## Check if node is leaf. - node.node.isLeaf +func newAllorder[T](node: T, direction: AllorderDirection): Allorder[T] = + Allorder[T](node:node, direction:direction) -func isRoot*[T](node: NewickOrderNode[T]): bool = - node.node.isRoot - -proc `$`*[T](node: NewickOrderNode[T]): string = - $node.node & ", " & $node.state - -iterator newickorder*[T: TraversableNode](root: T): NewickOrderNode[T] = - ## Newick order traverse. All internal nodes are visited twice. Leaf nodes are - ## only visited once. This traverese is a hybrid between preorder and - ## postorder traverse. It is convenient for writing newick strings and - ## plotting trees. - var stack: seq[NewickOrderNode[T]] - stack.add(newNewickOrderNode(root, descendingTree)) - stack.add(newNewickOrderNode(root, ascendingTree)) +iterator allorder*[T: TraversableNode](root: T): Allorder[T] = +# iterator allorder*[T](root: T): tuple[node:T, direction:AllorderDirection] = + ## All order traverse. Combined preorder/postorder traverse. All leaf nodes + ## are visited once in preorder direction (Ascending). All internal nodes are + ## visited twice. + var stack: seq[Allorder[T]] + # var stack: seq[tuple[node: T, direction: AllorderDirection]] + stack.add(newAllorder(root, descendingTree)) + stack.add(newAllorder(root, ascendingTree)) + # stack.add((root, descendingTree)) + # stack.add((root, ascendingTree)) while stack.len > 0: - var node = stack.pop() - yield node - if not node.isLeaf: - if node.state == ascendingTree: - for child in node.children.reversed: + var allorderNode = stack.pop() + yield allorderNode + if not allorderNode.node.isLeaf: + if allorderNode.direction == ascendingTree: + let children = allorderNode.node.children + for i in countdown(children.len - 1 , 0): + let child = children[i] if not child.isLeaf: - stack.add(newNewickOrderNode(child, descendingTree)) - stack.add(newNewickOrderNode(child, ascendingTree)) + stack.add(newAllorder(child, descendingTree)) + stack.add(newAllorder(child, ascendingTree)) + # stack.add((child, descendingTree)) + # stack.add((child, ascendingTree)) else: - stack.add(newNewickOrderNode(child, ascendingTree)) + stack.add(newAllorder(child, ascendingTree)) + # stack.add((child, ascendingTree)) \ No newline at end of file diff --git a/testNewick.nim b/testNewick.nim index b1cbd96..98dce94 100644 --- a/testNewick.nim +++ b/testNewick.nim @@ -1,28 +1,29 @@ import ./src/phylogeni -block: - var t = parseNewickString("((d:1.0,e:1.0)c:1.0,b:1.0)a:1.0;") - echo t.ascii - t.ladderize() - echo t.ascii - echo t.isRoot - var - e = t.find("e") - echo e.isLeaf - var - f = NHNode(label:"f", length:1.0) - g = NHNode(label:"g", length:1.0) - e.addChild(f) - e.addChild(g) - echo t.ascii - var - d = t.find("d") - c = getMRCA(e, d) - echo c - - prune(d) - echo t.ascii - echo t.writeNewickString() +# block: +# var t = parseNewickString("((d:1.0,e:1.0)c:1.0,b:1.0)a:1.0;") +# echo t.ascii + + # t.ladderize() + # echo t.ascii + # echo t.isRoot + # var + # e = t.find("e") + # echo e.isLeaf + # var + # f = NHNode(label:"f", length:1.0) + # g = NHNode(label:"g", length:1.0) + # e.addChild(f) + # e.addChild(g) + # echo t.ascii + # var + # d = t.find("d") + # c = getMRCA(e, d) + # echo c + + # prune(d) + # echo t.ascii + # echo t.writeNewickString() @@ -31,7 +32,56 @@ block: # s ="(b:1.0[&&NHX:key=b],(d:1.0[&&NHX:key=d],e:1.0[&&NHX:key=e])c:1.0[&&NHX:key=c])a:1.0[&&NHX:key=a];" # t = parseNewickString(s, NHXNode) # echo t.ascii -# for i in t.preorder(): -# echo i.data["key"] + # for i in t.preorder(): + # echo i.data["key"] # i.data["length"] = $i.length -# echo t.writeNewickString() \ No newline at end of file +# echo t.writeNewickString() + +import npeg + +# proc parseDataBlock[T](nex: var Nexus[T], str: string) = +proc parseDataBlock(str: string) = + # var dataBlock = NexusBlock[T](kind:nexusData) + # genericBugWorkAround() + let p = peg "data": + s <- *Space + S <- +Space + # label <- S * >+(Alnum | '_'): + # taxaBlock.taxa.add(capture[1].s) + # labels <- i"taxlabels" * +label * s * ';' + # dimensions <- i"dimensions" * S * i"ntax=" * >*Digit * ';': + # taxaBlock.ntaxa = parseInt(capture[1].s) + nchar <- S * >i"nchar=" * >+Digit + ntax <- S * >i"ntax=" * >+Digit + dimensions <- s * >i"dimensions" * ntax * nchar * s * ';' + + datatype <- S * >i"datatype=" * >+Alpha + missing <- S * >i"missing=" * >'?' + gap <- S * >i"gap=" * >'-' + format <- s * >i"format" * datatype * missing * gap * s * ';' + + sample <- S * >+Alpha * S * >+(Alpha | {'-', '?'}) + matrix <- s * >i"matrix" * +sample * s * ';' + + data <- dimensions * format * matrix + let r = p.match(str) + echo r.captures + # nex.add(taxaBlock) + + + +let str = """ +dimensions ntax=5 nchar=54; +format datatype=dna missing=? gap=-; +matrix + Ephedra TTAAGCCATGCATGTCTAAGTATGAACTAATTCCAAACGGTGAAACTGCGGATG + Gnetum TTAAGCCATGCATGTCTATGTACGAACTAATC-AGAACGGTGAAACTGCGGATG + Welwitschia TTAAGCCATGCACGTGTAAGTATGAACTAGTC-GAAACGGTGAAACTGCGGATG + Ginkgo TTAAGCCATGCATGTGTAAGTATGAACTCTTTACAGACTGTGAAACTGCGAATG + Pinus TTAAGCCATGCATGTCTAAGTATGAACTAATTGCAGACTGTGAAACTGCGGATG +; +""" + + + +parseDataBlock(str) From df380f9d7534d1c6436bc504817c6ecded636658 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Thu, 5 Oct 2023 18:17:55 -0500 Subject: [PATCH 11/13] Added undefined block parsing for nexus files --- src/phylogeni/nexusParser.nim | 69 ++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/phylogeni/nexusParser.nim b/src/phylogeni/nexusParser.nim index 608caa1..b4580d9 100644 --- a/src/phylogeni/nexusParser.nim +++ b/src/phylogeni/nexusParser.nim @@ -129,6 +129,12 @@ proc parseTreesBlock[T](nex: var Nexus[T], str: string) = assert r.ok nex.blocks.add(treeBlock) +proc parseUndefinedBlock[T](nex: var Nexus[T], name, str: string) = + var undefinedBlock = NexusBlock[T](kind:nexusUndefined) + undefinedBlock.blockName = name + undefinedBlock.blockString = str + nex.blocks.add(undefinedBlock) + proc parseNexusString*(str: string, T: typedesc[TraversableNode] = NexusNode): Nexus[T] = genericBugWorkAround() var nex = Nexus[T]() @@ -142,9 +148,7 @@ proc parseNexusString*(str: string, T: typedesc[TraversableNode] = NexusNode): N trees <- i"trees;" * >*(1-i"end;"): parseTreesBlock(nex, capture[1].s) undefined <- >+Alpha * ';' * >*(1 - i"end;"): - echo capture[1].s - echo capture[2].s - # parseUndefinedBlock(nex, capture[1].s) + parseUndefinedBlock(nex, capture[1].s, capture[2].s) kind <- (data | taxa | trees | undefined) nblock <- i"begin" * S * kind * S * i"end;" nexus <- i"#nexus" * S * nblock * *(S * nblock) * S * !1 @@ -157,34 +161,31 @@ proc parseNexusFile*(path: string, T: typedesc[TraversableNode] = NexusNode): Ne result = parseNexusString(str, T) -var str = """ -#NEXUS -Begin TAXA; - Dimensions ntax=4; - TaxLabels SpaceDog SpaceCat SpaceOrc SpaceElf; -End; - -Begin data; -dimensions ntax=5 nchar=54; -format datatype=dna missing=? gap=-; -matrix - Ephedra TTAAGCCATGCATGTCTAAGTATGAACTAATTCCAAACGGTGAAACTGCGGATG - Gnetum TTAAGCCATGCATGTCTATGTACGAACTAATC-AGAACGGTGAAACTGCGGATG - Welwitschia TTAAGCCATGCACGTGTAAGTATGAACTAGTC-GAAACGGTGAAACTGCGGATG - Ginkgo TTAAGCCATGCATGTGTAAGTATGAACTCTTTACAGACTGTGAAACTGCGAATG - Pinus TTAAGCCATGCATGTCTAAGTATGAACTAATTGCAGACTGTGAAACTGCGGATG -; -End; - -BEGIN TREES; - Tree tree1 = (((SpaceDog,SpaceCat),SpaceOrc,SpaceElf)); -END; - -BEGIN PAUP; - Dumb paup commands -END; -""" - - -var n = parseNexusString(str) -echo n \ No newline at end of file +# Used for testing +# var str = """ +# #NEXUS +# Begin TAXA; +# Dimensions ntax=4; +# TaxLabels SpaceDog SpaceCat SpaceOrc SpaceElf; +# End; + +# Begin data; +# dimensions ntax=5 nchar=54; +# format datatype=dna missing=? gap=-; +# matrix +# Ephedra TTAAGCCATGCATGTCTAAGTATGAACTAATTCCAAACGGTGAAACTGCGGATG +# Gnetum TTAAGCCATGCATGTCTATGTACGAACTAATC-AGAACGGTGAAACTGCGGATG +# Welwitschia TTAAGCCATGCACGTGTAAGTATGAACTAGTC-GAAACGGTGAAACTGCGGATG +# Ginkgo TTAAGCCATGCATGTGTAAGTATGAACTCTTTACAGACTGTGAAACTGCGAATG +# Pinus TTAAGCCATGCATGTCTAAGTATGAACTAATTGCAGACTGTGAAACTGCGGATG +# ; +# End; + +# BEGIN TREES; +# Tree tree1 = (((SpaceDog,SpaceCat),SpaceOrc,SpaceElf)); +# END; + +# BEGIN PAUP; +# Dumb paup commands +# END; +# """ \ No newline at end of file From 89b2f14a8ef39eaaab6a0731f47fac8d720a8d27 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Fri, 6 Oct 2023 13:15:55 -0500 Subject: [PATCH 12/13] Allow hyphen in taxa labels --- src/phylogeni/nexusParser.nim | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/phylogeni/nexusParser.nim b/src/phylogeni/nexusParser.nim index b4580d9..a5fd34b 100644 --- a/src/phylogeni/nexusParser.nim +++ b/src/phylogeni/nexusParser.nim @@ -101,12 +101,12 @@ proc parseTaxaBlock[T](nex: var Nexus[T], str: string) = let p = peg "taxa": s <- *Space S <- +Space - label <- S * >+(Alnum | '_'): + label <- S * >+(Alnum | {'_', '-'}): taxaBlock.taxa.add(capture[1].s) labels <- i"taxlabels" * +label * s * ';' dimensions <- i"dimensions" * S * i"ntax=" * >*Digit * ';': taxaBlock.ntaxa = parseInt(capture[1].s) - taxa <- s * dimensions * S * labels + taxa <- s * dimensions * S * labels * s * !1 let r = p.match(str) assert r.ok nex.add(taxaBlock) @@ -115,16 +115,16 @@ proc parseTreesBlock[T](nex: var Nexus[T], str: string) = var treeBlock = NexusBlock[T](kind:nexusTrees) genericBugWorkAround() let p = peg "trees": - S <- *Space + s <- *Space label <- *(Alnum | {'_', '.', '-'}) - pair <- S * >?label * S * >label: + pair <- s * >?label * s * >label: treeBlock.translate[capture[1].s] = capture[2].s - paired <- pair * *(S * ',' * pair) + paired <- pair * *(s * ',' * pair) translate <- >i"translate" * paired * @';' - tree <- S * i"tree" * S * >label * S * '=' * >@';': + tree <- s * i"tree" * s * >label * s * '=' * >@';': var t = parseNewickString(capture[2].s, T) treeBlock.trees.add((capture[1].s, t)) - trees <- S * ?translate * +tree + trees <- s * ?translate * +tree * s * !1 let r = p.match(str) assert r.ok nex.blocks.add(treeBlock) @@ -161,7 +161,7 @@ proc parseNexusFile*(path: string, T: typedesc[TraversableNode] = NexusNode): Ne result = parseNexusString(str, T) -# Used for testing +# # Used for testing # var str = """ # #NEXUS # Begin TAXA; @@ -182,10 +182,18 @@ proc parseNexusFile*(path: string, T: typedesc[TraversableNode] = NexusNode): Ne # End; # BEGIN TREES; -# Tree tree1 = (((SpaceDog,SpaceCat),SpaceOrc,SpaceElf)); +# Translate 1 SpaceDog, 2 SpaceCat, 3 SpaceOrc, 4 SpaceElf; +# Tree tree1 = [&R] (((SpaceDog[&length_range={243.375355,284.829584},height_95%_HPD={1.9999999949504854E-6,6.000000098538294E-6},length_95%_HPD={252.659576,279.50401},length=268.28712902000007,length_median=268.7639845,height_median=4.00000001832268E-6,height_range={2.2737367544323206E-13,6.99999992548328E-6},height=3.8000000552074197E-6],SpaceCat),SpaceOrc,SpaceElf)); # END; # BEGIN PAUP; # Dumb paup commands # END; -# """ \ No newline at end of file +# """ + +# # import ./traverse + +# # var n = parseNexusString(str) +# # var t = n[2].trees[0].tree +# # for i in t.preorder(): +# # echo i.data \ No newline at end of file From c2c5a19dfe34862a25db5352567b4b8ae3e90cb1 Mon Sep 17 00:00:00 2001 From: Kerry Cobb Date: Fri, 22 Mar 2024 09:41:40 -0500 Subject: [PATCH 13/13] Cleanup old files --- old-src/phylogeni.nim | 114 ---------- old-src/phylogeni/drawing.nim | 78 ------- old-src/phylogeni/io/parseNewick.nim | 303 --------------------------- old-src/phylogeni/io/writeNewick.nim | 38 ---- old-src/phylogeni/simulate.nim | 82 -------- old-src/phylogeni/tree.nim | 241 --------------------- src/phylogeni/coordinates.nim | 2 +- testNewick.nim | 87 -------- tests/test_parseNewick.nim | 72 ------- tests/test_simulate.nim | 23 -- tests/test_tree.nim | 67 ------ tests/test_writeNewick.nim | 46 ---- 12 files changed, 1 insertion(+), 1152 deletions(-) delete mode 100644 old-src/phylogeni.nim delete mode 100644 old-src/phylogeni/drawing.nim delete mode 100644 old-src/phylogeni/io/parseNewick.nim delete mode 100644 old-src/phylogeni/io/writeNewick.nim delete mode 100644 old-src/phylogeni/simulate.nim delete mode 100644 old-src/phylogeni/tree.nim delete mode 100644 testNewick.nim delete mode 100644 tests/test_parseNewick.nim delete mode 100644 tests/test_simulate.nim delete mode 100644 tests/test_tree.nim delete mode 100644 tests/test_writeNewick.nim diff --git a/old-src/phylogeni.nim b/old-src/phylogeni.nim deleted file mode 100644 index 6a25368..0000000 --- a/old-src/phylogeni.nim +++ /dev/null @@ -1,114 +0,0 @@ -import ./phylogeni/[ - tree, - io/parseNewick, - io/writeNewick, - simulate - ] - -export tree, - parseNewick, - writeNewick, - simulate - -## ========= -## PhylogeNi -## ========= -## -## PhylogeNi is a Nim library for working with phylogenetic trees. -## - -runnableExamples: - var t = treeFromString("(B:1.0,C:1.0)A:1.0;") - - echo t - - # -A /-B - # \-C - - for i in t.preorder(): - if i.label == "C": - i.addChild(newNode("D", 1.0)) - i.addChild(newNode("E", 1.0)) - t.ladderize(Ascending) - echo t - - # /C /-D - # -A| \-E - # \-B - - var str = t.writeNewickString() - echo str - # [&U]((D:1.0,E:1.0)C:1.0,B:1.0)A:1.0; - -## -## See the module docs for more details: -## `tree<./phylogeni/tree.html>`_ -## Provides basic functions for working with `Tree` and `Node` types such as: -## - Tree and Node creation -## - Topology modification -## - Tree iteration -## -## `parseNewick<./phylogeni/io/parseNewick.html>`_ -## Provides functions for reading trees from files or strings. -## -## `writeNewick<./phylogeni/io/writeNewick.html>`_ -## Provides functions for writing trees to files or strings. -## -## `simulate<./phylogeni/tree.html>`_ -## Provides functions for simulating trees: -## - Pure birth model -## - Birth death model -## -## Generic Node Data -## ================= -## `Node` is a generic type which can have any object stored in the data field. -## -## One great feature of PhylogeNi is that you do not need to completely rewrite your -## own parser/writer for custom data types when reading and writing a newick file or string. -## You only need to create `parseAnnotation` and `writeAnnotation` procs to handle -## reading or writing the annotation string. - -runnableExamples: - import std/strutils - import std/strformat - - type - CustomData = object - posterior: float - credibleInterval: tuple[lower, upper: float] - - let treeStr = "(B:1.0[&p:0.95,ci:0.9-1.0],C:1.0[&p:0.95,ci:0.9-1.0])A:1.0[&p:0.95,ci:0.9-1.0];" - - proc parseAnnotation(p: var NewickParser[CustomData], annotation: string) = - let annotations = annotation.split(",") - var dataCheck = (p: false, ci: false) - for i in annotations: - let split = i.split(":") - doAssert split.len == 2 - case split[0] - of "p": - p.currNode.data.posterior = parseFloat(split[1]) - dataCheck.p = true - of "ci": - let ci = split[1].split("-") - doAssert ci.len == 2 - p.currNode.data.credibleInterval = (parseFloat(ci[0]), parseFloat(ci[1])) - dataCheck.ci = true - else: - raise newException(NewickError, "Invalid Annotation") - if not dataCheck.p or not dataCheck.ci: - raise newException(NewickError, "") - - proc writeAnnotation(node: Node[CustomData], str: var string) = - str.add(fmt"[&p:{$node.data.posterior},ci:{$node.data.credibleInterval.lower}-{$node.data.credibleInterval.upper}]") - - let - t = treeFromString(treeStr, CustomData) - str = t.writeNewickString() - echo str - # [&U](B:1.0[&p:0.95,ci:0.9-1.0],C:1.0[&p:0.95,ci:0.9-1.0])A:1.0[&p:0.95,ci:0.9-1.0]; - - - - - diff --git a/old-src/phylogeni/drawing.nim b/old-src/phylogeni/drawing.nim deleted file mode 100644 index 33e2e06..0000000 --- a/old-src/phylogeni/drawing.nim +++ /dev/null @@ -1,78 +0,0 @@ -# Inherit from Node and only borrow the necessary procs -# Leave out or change some setters and getters - -import ./tree - -import phylogeni - -type - DrawNode*[T] = ref object - x: float # Horizontal position of node, equivalent to node height - y: float # Vertical position of node - data: T - -# proc x*[T](n: DrawNode[T]): float = -# result = n.x - -# proc y*[T](n: DrawNode[T]): float = -# result = n.y - -# proc data*[T](n: DrawNode[T]): T = -# result = n.data - -template toClosure*(i): auto = - ## Wrap an inline iterator in a first-class closure iterator. - iterator j: type(i) {.closure.} = - for x in i: yield x - j - -proc copyToDrawNodeTree[T](tree: Node[T]): Node[DrawNode[T]] = - ## Copy tree structure and replace existing data with DrawNode type with - ## data being copied to the DrawNode data property - var copied = Node[DrawNode[T]](length:tree.length, label:tree.label, data:DrawNode[T](data:tree.data)) - for i in tree.children: - copied.addChild(copyToDrawNodeTree(i)) - result = copied - -proc getCoords*[T](tree: Node[T], branchLengthScaling=1.0, branchSepScaling=1.0): Node[DrawNode[T]] = - ## Return coordinates for a typical rectangular or slanted phylogeny - # TODO: Raise Error if branchLengthScaling or branchSepScaling is <=0 - var copied = copyToDrawNodeTree(tree) - - # Make newickorder a closure iterator using template - let newickOrderIt = toClosure(copied.newickorder) - - # Iter over nodes in newick order. Assign x on first pass of all nodes. - # Assign y when visiting leaves and second visit of each node. - var - root = newickOrderIt().node - leafY = 0.0 - root.data = DrawNode[T]() - root.data.x = root.length * branchSepScaling - for i in newickOrderIt(): - var n = i.node - if i.firstVisit: - # Assign x on first visit - n.data.x = n.parent.data.x + (n.length * branchLengthScaling) - # Assign y to leaves - if i.node.isLeaf: - n.data.y = leafY - leafY += branchSepScaling - else: - # Assign y on second visit of each internal node - if not n.isLeaf: - let - lo = n.children[0].data.y - up = n.children[^1].data.y - n.data.y = (up - lo) / 2 + lo - result = copied - -let t = parseNewickString("(B:1.0[Test],((E:1.0,F:1.0)D:1.0[Test],G:1.0)C:1.0)A:1.0;", typ=string) -let c = getCoords(t) -echo t.ascii -for i in c.preorder: - echo i.label, ", ", i.data.x, ", ", i.data.y -echo "" -let c2 = getCoords(t, branchLengthScaling=2.0, branchSepScaling=2.0) -for i in c2.preorder: - echo i.label, ", ", i.data.x, ", ", i.data.y diff --git a/old-src/phylogeni/io/parseNewick.nim b/old-src/phylogeni/io/parseNewick.nim deleted file mode 100644 index f1555ac..0000000 --- a/old-src/phylogeni/io/parseNewick.nim +++ /dev/null @@ -1,303 +0,0 @@ -# TODO: Should rewrite this a bit to be more constraining and to catch more errors -# before Nim does, such as when reading "A,B;":. Also regret allowing annotations -# to occur anywhere which will be problematic if I make trees generic and -# parseAnnotation mixins get called before the label and length is parsed. - -# TODO: String annotation is not currently being parsed - -import std/[streams, lexbase, strformat, strutils] -import ../tree - -type - NewickError* = object of IOError - - NewickState = enum - newickStart, newickTopology, newickLabel, newickLength, newickEnd, newickEOF - # TODO: This might be a better way to track state in order to raise errors if - # a newick string doesn't have any parentheses. Low priority given how - # unlikely that is. - # newickStart, newickStartLabel, newickStartLength, newickStartTopology, - # newickTopology, newickLabel, newickLength, newickEnd, newickEOF - - NewickParser*[T] = object of BaseLexer - root: Node[T] - currNode*: Node[T] - token: string - state: NewickState - annotationState: bool # False if an annotation has already been parsed - -const newickWhitespace = {' ', '\t', '\c', '\l'} - -proc raiseError[T](p: NewickParser[T], msg: string) = - var - lineNum = $p.lineNumber - colNum = $p.getColNumber(p.bufpos+1) - m = fmt"{msg} at line {lineNum}, column {colNum}" - raise newException(NewickError, m) - -proc parseWhitespace[T](p: var NewickParser[T], skip=true) = - while true: - case p.buf[p.bufpos] - of ' ', '\t': - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - of '\c': - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos = lexbase.handleCR(p, p.bufpos) - of '\l': # same as \n - if not skip: p.token.add(p.buf[p.bufpos]) - p.bufpos = lexbase.handleLF(p, p.bufpos) - else: - break - -proc parseAnnotation(p: var NewickParser[string], annotation: string) = - p.currNode.data = annotation - -proc parseAnnotation(p: var NewickParser[void], annotation: string) = - discard - -proc parseBracket[T](p: var NewickParser[T], showComments=false) = - # TODO: handle unexpected end of file and newick statement - mixin parseAnnotation - p.token = "" - p.bufpos.inc() - while true: - case p.buf[p.bufpos] - of ']': - p.bufpos.inc() - break - of newickWhitespace: - p.parseWhitespace(skip=false) - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - if p.token.startswith('&'): - if p.annotationState: - p.parseAnnotation(p.token[1..^1]) - p.annotationState = false - else: - if showComments: - echo p.token - -proc parseLength[T](p: var NewickParser[T]) = - var parseLength = true - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';': - p.state = newickTopology - break - of newickWhitespace: - p.parseWhitespace() - of '[': - p.parseBracket() - of EndOfFile: - p.raiseError("Unexpected end of stream") - else: - if parseLength: - p.token = "" - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';', '[', newickWhitespace, EndOfFile: - parseLength = false - break - of '"': - p.raiseError("Unexpected \"") - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.length = parseFloat(p.token) - parseLength = false - -proc parseLabel[T](p: var NewickParser[T]) = - var parseLabel = true - p.annotationState = true - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';': - p.state = newickTopology - break - of ':': - p.state = newickLength - p.bufpos.inc() - break - of '[': - p.parseBracket() - of newickWhitespace: - p.parseWhitespace() - of EndOfFile: - p.raiseError("Unexpected end of stream") - of '"': - # Parse quoted text - if parseLabel: - p.token = "" - p.bufpos.inc() - while true: - case p.buf[p.bufpos] - of '"': - p.bufpos.inc() - break - of newickWhitespace: - p.parseWhitespace(skip=false) - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.label = p.token - parseLabel = false - else: - p.raiseError("Unexpected \"") - else: - # Parse unquoted text - if parseLabel: - p.token = "" - while true: - case p.buf[p.bufpos] - of '(', ',', ')', ';', ':', '[', ']', newickWhitespace, EndOfFile: - parseLabel = false - break - of '"': - p.raiseError("Unexpected \"") - else: - p.token.add(p.buf[p.bufpos]) - p.bufpos.inc() - p.currNode.label = p.token - parseLabel = false - else: - p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - -proc parseTopology[T](p: var NewickParser[T]) = - # Parse newick tree - case p.buf[p.bufpos] - of '(': - var newNode = Node[T]() - p.currNode.addChild(newNode) - p.currNode = newNode - p.bufpos.inc() - p.state = newickLabel - of ',': - var newNode = Node[T]() - p.currNode.parent.addChild(newNode) - p.currNode = newNode - p.bufpos.inc() - p.state = newickLabel - of ')': - p.currNode = p.currNode.parent - p.bufpos.inc() - p.state = newickLabel - of ';': - if p.currNode == p.root: - p.bufpos.inc() - p.state = newickEnd - else: - p.raiseError("Mismatched parentheses") - else: - p.raiseError(&"Internal error, report possible bug") - -proc parseStart[T](p: var NewickParser[T]) = - # Parse beginning of newick file - while true: - case p.buf[p.bufpos] - of '(': - p.state = newickTopology - break - of ',': - p.raiseError("Unexpected comma. There can be only one root node.") - of newickWhitespace: - p.parseWhitespace() - of '[': - if p.buf[p.bufpos+1] == '&': - case p.buf[p.bufpos+2] - of 'r', 'R': - # p.tree.rooted = true - discard - of 'u', 'U': - # p.tree.rooted = false - discard - else: - p.bufpos.inc(2) - p.raiseError(&"Unexpected character \"{p.buf[p.bufpos]}\"") - if p.buf[p.bufpos+3] == ']': - p.bufpos.inc(4) - else: - p.bufpos.inc(3) - p.raiseError("Expected \"]\"") - else: - p.parseBracket() - of EndOfFile: - # p.state = newickEOF - # break - p.raiseError("Unexpected end of file. No newick statment found.") - else: - p.state = newickLabel - break - -proc parseTree[T](p: var NewickParser[T]) = - p.parseWhitespace() - while true: - case p.state - of newickStart: - p.parseStart() - of newickTopology: - p.parseTopology() - of newickLabel: - p.parseLabel() - of newickLength: - p.parseLength() - of newickEnd: - break - of newickEOF: - break - -proc parseNewickStream*(stream: Stream, typ: typedesc = void): Node[typ] = - ## Parse a newick stream - var - p = NewickParser[typ]() - p.root = Node[typ]() - p.currNode = p.root - p.open(stream) - p.parseTree() - p.close() - result = p.root - -# proc parseNewickStream*[T](treeSeq: var TreeSeq[T], stream: Stream) = -# ## Parse a newick stream -# var -# p = NewickParser[T]() -# p.open(stream) -# while true: -# p.state = newickStart -# p.tree = Tree[T]() -# p.tree.root = Node[T]() -# p.currNode = p.tree.root -# p.parseTree() -# case p.state -# of newickEOF: -# break -# of newickEnd: -# treeSeq.add(p.tree) -# else: -# p.raiseError("Internal error, report possible bug") -# p.close() - -proc parseNewickString*(str: string, typ: typedesc = void): Node[typ] = - ## Parse a newick string - var ss = newStringStream(str) - result = parseNewickStream(ss, typ) - ss.close() - -# proc parseNewickString*[T](treesSeq: var TreeSeq[T], str: string) = -# ## Parse a newick string -# var ss = newStringStream(str) -# treesSeq.parseNewickStream(ss) -# ss.close() - -proc parseNewickFile*(path: string, typ: typedesc = void): Node[typ] = - ## Parse a newick file - var fs = newFileStream(path, fmRead) - result = parseNewickStream(fs, typ) - fs.close() - -# proc parseNewickFile*[T](treeSeq: var TreeSeq[T], path: string) = -# ## Parse a newick file -# var fs = newFileStream(path, fmRead) -# treeSeq.parseNewickStream(fs) -# fs.close() \ No newline at end of file diff --git a/old-src/phylogeni/io/writeNewick.nim b/old-src/phylogeni/io/writeNewick.nim deleted file mode 100644 index 88735a2..0000000 --- a/old-src/phylogeni/io/writeNewick.nim +++ /dev/null @@ -1,38 +0,0 @@ -import ../tree -import std/strformat - -func writeNewickData(node: Node[string], str: var string) = - str.add(fmt"[&{node.data}]") - -func writeNewickData(node: Node[void], str: var string) = - discard - -func writeNewickData[T](node: Node[T], str: var string) = - mixin writeAnnotation - str.add(node.label) - str.add(fmt":{$node.length}") - node.writeAnnotation(str) - -func writeNewickString*[T](tree: Node[T]): string = - ## Write newick string for Node object - var str = "" - for i in tree.newickorder(): - if i.firstVisit == true: - if i.node.isLeaf(): - str.add(i.node.writeNewickData) - if i.node != i.node.parent.children[^1]: # not the first node in parents children - str.add(",") - else: # is internal node - str.add("(") - else: # is second visit to node - str.add(")") - str.add(i.node.writeNewickData) - if (i.node != tree) and (i.node != i.node.parent.children[^1]): # is not last node in parents children - str.add(",") - str.add(";") - result = str - -proc writeNewickFile*[T](tree: Node[T], filename:string) = - # Write a newick file for Node object - var str = writeNewickString(tree) - writeFile(filename, str) diff --git a/old-src/phylogeni/simulate.nim b/old-src/phylogeni/simulate.nim deleted file mode 100644 index 5be4255..0000000 --- a/old-src/phylogeni/simulate.nim +++ /dev/null @@ -1,82 +0,0 @@ -import std/[random, math] -import ./tree - -# TODO: Make BirthDeath Simulator Work -# TODO: Make option to take random number generator object as an option - -proc randExp(l: float): float = - -ln(rand(1.0))/l - -proc uniformPureBirth*(nTips: int, birthRate: float=1.0, typ=void): Node[typ] = - ## Simulate tree under uniform pure birth process. - var - t = Node[typ]() - leaves = @[t] - for i in 1 ..< nTips: - var - waitTime = randExp(float(leaves.len()) * birthRate) - rLeaf = rand(leaves.len - 1) - # Add wait time to all leaves - for node in leaves: - node.length += waitTime - # Add descendant nodes to random leaf - for i in 0..1: - var nd = Node[typ]() - leaves[rLeaf].addChild(nd) - leaves.add(nd) - # Remove previous random leaf from leaf list since it is now internal node - leaves.delete(rLeaf) - # Add additional length and tip labels to final leaves - var - waitTime = randExp(float(leaves.len()) * birthRate) - inc = 1 - for node in leaves: - node.length += waitTime - node.label = "T" & $inc - inc += 1 - result = t - -proc uniformBirthDeath*(nTips: int, birthRate=1.0, deathRate=1.0, rerun=false, typ=void): Node[typ] = - ## Simulate tree under uniform birth death process. - var - t = Node[typ]() - leaves = @[t] - while true: - if leaves.len() == nTips: - break - var - waitTime = randExp(float(leaves.len()) * (birthRate + deathRate)) - rLeaf = rand(leaves.len - 1) - # Add wait time to all leaves - for node in leaves: - node.length += waitTime - # Determine if speciation or extinction even - if rand(1.0) < birthRate / (birthRate + deathRate): - # Speciation event - for i in 0..1: - var nd = Node[typ]() - leaves[rLeaf].addChild(nd) - leaves.add(nd) - else: - # Extinction event - if leaves.len() == 1: - # Rerun - if rerun: - leaves.add(t) - # Or quit - else: - break - else: - t.prune(leaves[rLeaf]) - # Delete random leaf from leaf list - leaves.delete(rLeaf) - # Add additional length and tip labels to final leaves - var - waitTime = randExp(float(leaves.len()) * birthRate) - inc = 1 - for node in leaves: - node.length += waitTime - node.label = "T" & $inc - inc += 1 - result = t - diff --git a/old-src/phylogeni/tree.nim b/old-src/phylogeni/tree.nim deleted file mode 100644 index c9c8ea2..0000000 --- a/old-src/phylogeni/tree.nim +++ /dev/null @@ -1,241 +0,0 @@ -#TODO: Make Node attributes private and make setters and getters -# or make Node a concept - -import std/[algorithm, tables, hashes, strutils, sequtils] - -export algorithm.SortOrder - -type - Node*[T] = ref object - parent*: Node[T] - children*: seq[Node[T]] - label*: string - length*: float - data*: T - - TreeError* = object of CatchableError - -func hash*[T](n: Node[T]): Hash = - result = n.label.hash !& n.length.hash - result = !$result - -func addChild*[T](parent: Node[T], newChild: Node[T]) = - ## Add child node to parent. - newChild.parent = parent - parent.children.add(newChild) - -func addSister*[T](node: Node[T], newSister: Node[T]) = - ## Add sister node. - newSister.parent = node.parent - node.parent.children.add(newSister) - -func isLeaf*[T](node: Node[T]): bool = - ## Check if node is leaf. - if node.children.len == 0: - result = true - else: - result = false - -func isRoot*[T](node: Node[T]): bool = - if node.parent == nil: - result = true - else: - result = false - -func prune*[T](tree, node: Node[T]) = - ## Prune branch leading to node from tree. - if node.parent == nil: - raise newException(TreeError, "Cannot prune root node") - var parent = node.parent - parent.children.delete(parent.children.find(node)) - if parent.children.len() == 1: - var child = parent.children[0] - parent.length += child.length - parent.children = child.children - parent.label = child.label - -proc copyTree*[T](tree: Node[T], typ: typedesc = void): Node[typ] = - ## Copy the structure, edge lengths, and labels of a tree. The returned tree - ## may have a different data type. - var copied = Node[typ](length:tree.length, label:tree.label) - for i in tree.children: - copied.addChild(copyTree(i, typ)) - result = copied - -iterator preorder*[T](root: Node[T]): Node[T] = - ## Preorder traverse. - var stack = @[root] - while stack.len > 0: - var node = stack.pop() - stack.add(node.children.reversed()) - yield node - -iterator postorder*[T](root: Node[T]): Node[T] = - ## Postorder traverse. - var - preStack = @[root] - postStack: seq[Node[T]] - while preStack.len > 0: - var node = preStack.pop() - postStack.add(node) - preStack.add(node.children) - while postStack.len > 0: - var node = postStack.pop() - yield node - -iterator newickorder*[T](root: Node[T]): tuple[node:Node[T], firstVisit:bool] = - ## Newick order traverse. All internal nodes are visited twice. - var stack: seq[tuple[node: Node[T], firstVisit: bool]] - stack.add((node: root, firstVisit: false)) - stack.add((node: root, firstVisit: true)) - while stack.len > 0: - var nodeTuple = stack.pop() - yield (nodeTuple) - if nodeTuple.node.children.len > 0: - if nodeTuple.firstVisit == true: - for child in nodeTuple.node.children.reversed: - if child.children.len > 0: - stack.add((child, false)) - stack.add((child, true)) - else: - stack.add((child, true)) - -iterator levelorder*[T](root: Node[T]): Node[T] = - ## Levelorder traverse. - yield root - var stack = root.children - while stack.len > 0: - var node = stack[0] - stack.delete(0) - yield node - stack.add(node.children) - -iterator iterleaves*[T](root: Node[T]): Node[T] = - ## Iter over leaves. - for i in root.preorder(): - if i.is_leaf(): - yield i - -func ladderize*[T](root: Node[T], order: SortOrder = Ascending) = - ## Ladderize subtree. - # TODO: Should reimplement with heap queue and without using table - var - nodeDescendantCount = initTable[Node[T], int]() - for node in root.postorder(): - if node.children.len == 0: - nodeDescendantCount[node] = 0 - else: - var total = 0 - for child in node.children: - total += nodeDescendantCount[child] - total += node.children.len - nodeDescendantCount[node] = total - node.children.sort( - cmp=func(a, b: Node[T]): int = cmp(nodeDescendantCount[b], - nodeDescendantCount[a]), order=order) - -func calcTreeLength*[T](node: Node[T]): float = - ## Calculate total length of tree. - result = 0.0 - for child in node.children: - for i in child.preorder(): - result += i.length - -func treeHeight*[T](node: Node[T]): float = - ## Calculate the height of subtree. - var maxHeight = 0.0 - for child in node.children: - let childHeight = treeHeight(child) - maxHeight = max(maxHeight, childHeight) - result = maxHeight + node.length - -func findNode*[T](tree: Node[T], str: string): Node[T] = - ## Returns first instance of node label matching str. - for i in tree.preorder: - if i.label == str: - return i - -func getAncestors*[T](node: Node[T]): seq[Node[T]] = - var curr = node - while true: - if curr.parent != nil: - result.add(curr.parent) - curr = curr.parent - else: - break - -func getMRCA*[T](a, b: Node[T]): Node[T] = - ## Get the most recent common ancestor of two nodes. - # TODO: I think this could be faster adding the elements of the shoter list to a - # hash set and then checking if the elements of the other list belong to that set - let - aAncestors = a.getAncestors - bAncestors = b.getAncestors - for i in aAncestors: - for j in bAncestors: - if i == j: - return i - raise newException(TreeError, "No MRCA shared by nodes") - -func get_ascii[T](node: Node[T], char1="-", showInternal=true): tuple[clines: seq[string], mid:int] = - ## Generates ascii string representation of tree. - var - len = 3 - if node.children.len == 0 or showInternal == true: - if node.label.len > len: - len = node.label.len - var - pad = strutils.repeat(' ', len) - pa = strutils.repeat(' ', len-1) - if node.children.len > 0: - var - mids: seq[int] - results: seq[string] - for child in node.children: - var char2: string - if node.children.len == 1: - char2 = "-" - elif child == node.children[0]: - char2 = "/" - elif child == node.children[^1]: - char2 = "\\" - else: - char2 = "-" - var (clines, mid) = get_ascii(child, char2, showInternal) - mids.add(mid+len(results)) - results.add(clines) - var - lo = mids[0] - hi = mids[^1] - last = len(results) - mid = int((lo+hi)/2) - prefixes: seq[string] - prefixes.add(sequtils.repeat(pad, lo+1)) - if mids.len > 1: - prefixes.add(sequtils.repeat(pa & "|", hi-lo-1)) - prefixes.add(sequtils.repeat(pad, last-hi)) - prefixes[mid] = char1 & strutils.repeat("-", len-2) & prefixes[mid][^1] - var new_results: seq[string] - for (p, r) in zip(prefixes, results): - new_results.add(p&r) - if showInternal: - var stem = new_results[mid] - new_results[mid] = stem[0] & node.label & stem[node.label.len+1..^1] - result = (new_results, mid) - else: - result = (@[char1 & "-" & node.label], 0) - -func ascii*[T](node: Node[T], char1="-", showInternal=true): string = - ## Returns ascii string representation of tree. - var (lines, _) = get_ascii(node, char1, showInternal) - result = lines.join("\n") - -func `$`*[T](node: Node[T]): string = - result = node.label - -# TODO: Implement these: -# func delete*(node: Node) = - ## Remove only this node and not parent or children - -# func extractTreeCopy*[T](node: Node[T]): Node[T] = - # Return copy of tree rooted at node. \ No newline at end of file diff --git a/src/phylogeni/coordinates.nim b/src/phylogeni/coordinates.nim index 8709e31..2653f3c 100644 --- a/src/phylogeni/coordinates.nim +++ b/src/phylogeni/coordinates.nim @@ -18,7 +18,7 @@ proc x*[T](n: CoordNode[T]): float = n.x proc y*[T](n: CoordNode[T]): float = - n.x + n.y proc node*[T](n: CoordNode[T]): T = n.node diff --git a/testNewick.nim b/testNewick.nim deleted file mode 100644 index 98dce94..0000000 --- a/testNewick.nim +++ /dev/null @@ -1,87 +0,0 @@ -import ./src/phylogeni - -# block: -# var t = parseNewickString("((d:1.0,e:1.0)c:1.0,b:1.0)a:1.0;") -# echo t.ascii - - # t.ladderize() - # echo t.ascii - # echo t.isRoot - # var - # e = t.find("e") - # echo e.isLeaf - # var - # f = NHNode(label:"f", length:1.0) - # g = NHNode(label:"g", length:1.0) - # e.addChild(f) - # e.addChild(g) - # echo t.ascii - # var - # d = t.find("d") - # c = getMRCA(e, d) - # echo c - - # prune(d) - # echo t.ascii - # echo t.writeNewickString() - - - -# block: -# var -# s ="(b:1.0[&&NHX:key=b],(d:1.0[&&NHX:key=d],e:1.0[&&NHX:key=e])c:1.0[&&NHX:key=c])a:1.0[&&NHX:key=a];" -# t = parseNewickString(s, NHXNode) -# echo t.ascii - # for i in t.preorder(): - # echo i.data["key"] -# i.data["length"] = $i.length -# echo t.writeNewickString() - -import npeg - -# proc parseDataBlock[T](nex: var Nexus[T], str: string) = -proc parseDataBlock(str: string) = - # var dataBlock = NexusBlock[T](kind:nexusData) - # genericBugWorkAround() - let p = peg "data": - s <- *Space - S <- +Space - # label <- S * >+(Alnum | '_'): - # taxaBlock.taxa.add(capture[1].s) - # labels <- i"taxlabels" * +label * s * ';' - # dimensions <- i"dimensions" * S * i"ntax=" * >*Digit * ';': - # taxaBlock.ntaxa = parseInt(capture[1].s) - nchar <- S * >i"nchar=" * >+Digit - ntax <- S * >i"ntax=" * >+Digit - dimensions <- s * >i"dimensions" * ntax * nchar * s * ';' - - datatype <- S * >i"datatype=" * >+Alpha - missing <- S * >i"missing=" * >'?' - gap <- S * >i"gap=" * >'-' - format <- s * >i"format" * datatype * missing * gap * s * ';' - - sample <- S * >+Alpha * S * >+(Alpha | {'-', '?'}) - matrix <- s * >i"matrix" * +sample * s * ';' - - data <- dimensions * format * matrix - let r = p.match(str) - echo r.captures - # nex.add(taxaBlock) - - - -let str = """ -dimensions ntax=5 nchar=54; -format datatype=dna missing=? gap=-; -matrix - Ephedra TTAAGCCATGCATGTCTAAGTATGAACTAATTCCAAACGGTGAAACTGCGGATG - Gnetum TTAAGCCATGCATGTCTATGTACGAACTAATC-AGAACGGTGAAACTGCGGATG - Welwitschia TTAAGCCATGCACGTGTAAGTATGAACTAGTC-GAAACGGTGAAACTGCGGATG - Ginkgo TTAAGCCATGCATGTGTAAGTATGAACTCTTTACAGACTGTGAAACTGCGAATG - Pinus TTAAGCCATGCATGTCTAAGTATGAACTAATTGCAGACTGTGAAACTGCGGATG -; -""" - - - -parseDataBlock(str) diff --git a/tests/test_parseNewick.nim b/tests/test_parseNewick.nim deleted file mode 100644 index 4ed1354..0000000 --- a/tests/test_parseNewick.nim +++ /dev/null @@ -1,72 +0,0 @@ -import ../src/phylogeni -import unittest -import strutils - -template toSeq(iter: untyped, param: untyped): untyped = - var s: seq[string] - for i in iter: - s.add($i.param) - s.join(" ") - -suite "Parse Valid Trees": - var t = Tree[string]() - test "valid 1": - t.parseNewickString(" [&r] (( [comment] C : 1.0 [&data] , D [comment] : 1.0 [&data] )B : [Comment] 1.0 [&data] )A : 1.0 [comment] [&data] ; ") - check(t.rooted) - check(toSeq(t.preorder, label) == "A B C D" ) - check(toSeq(t.preorder, length) == "1.0 1.0 1.0 1.0") - check(toSeq(t.preorder, data) == "data data data data") - - test "valid 2": - t.parseNewickString(" [&r] (( [&data] C : 1.0 , D [&data] : 1.0 ) B : [&data] 1.0 ) A : 1.0 [&data] ; ") - check(t.rooted) - check(toSeq(t.preorder, label) == "A B C D" ) - check(toSeq(t.preorder, length) == "1.0 1.0 1.0 1.0") - check(toSeq(t.preorder, data) == "data data data data") - - test "valid 3": - t.parseNewickString(" [&r] ( \"B B\" : 1.0 [&data] , \"C C\" : 1.0 [&data] ) \"A A\" : 1.0 [&data] ; ") - check(t.rooted) - check(toSeq(t.preorder, label) == "A A B B C C" ) - check(toSeq(t.preorder, length) == "1.0 1.0 1.0") - check(toSeq(t.preorder, data) == "data data data") - - # TODO: Write tests for these - # echo newTreeFromString(";") - # echo newTreeFromString("A;") - # echo newTreeFromString("();") - # echo newTreeFromString("(B)A;") - # echo newTreeFromString("(B,C,D)A;") - # echo newTreeFromString("((D,E)C,B)A;") - # echo newTreeFromString("((A,B),C);") - - - -test "Parse Invalid Trees": - var t = Tree[void]() - - proc testParse(str, expected: string) = - expect NewickError: - t.parseNewickString(str) - let msg = getCurrentExceptionMsg() - check(msg == expected) - - testParse("&r](B,C)A;", "Unexpected character \"]\" at line 1, column 3") - testParse("[&x](B,C)A;", "Unexpected character \"x\" at line 1, column 3") - testParse("[&r(B,C)A;", "Expected \"]\" at line 1, column 4") - testParse("[&r]((B,C,D)A;", "Mismatched parentheses at line 1, column 14") - testParse("[&r](B\",C)A;", "Unexpected \" at line 1, column 7") - testParse("[&r](B B,C)A;", "Unexpected character \"B\" at line 1, column 8") - testParse("[&r](\"B B\"\"B B\",\"C C\")\"A A\";", "Unexpected \" at line 1, column 11") - testParse("[&r](B,C)A", "Unexpected end of stream at line 1, column 11") - testParse("[&r](B:1.0,C:1.0)A:1.0", "Unexpected end of stream at line 1, column 23") - - # TODO: Write tests for these - # discard newTreeFromString("") - # discard newTreeFromString("A") - # discard newTreeFromString("()") - # discard newTreeFromString("(A,B;") - # discard newTreeFromString(",;") - - - \ No newline at end of file diff --git a/tests/test_simulate.nim b/tests/test_simulate.nim deleted file mode 100644 index 2ae7817..0000000 --- a/tests/test_simulate.nim +++ /dev/null @@ -1,23 +0,0 @@ -import ../src/phylogeni -import unittest - -# TODO: Randomly simulate large batch of trees and ensure that the mean branch length is close to the expectation - -suite "Tree Simulation": - test "pure birth": - var - t = uniformPureBirth(10) - i = 0 - for l in t.iterleaves(): - i+=1 - check(i == 10) - - - test "birth death": - var - t = uniformBirthDeath(10, rerun=true) - i = 0 - for l in t.iterleaves(): - i+=1 - check(i == 10) - diff --git a/tests/test_tree.nim b/tests/test_tree.nim deleted file mode 100644 index 8e5d3b6..0000000 --- a/tests/test_tree.nim +++ /dev/null @@ -1,67 +0,0 @@ -import ../src/phylogeni -import strutils -import unittest - -let - a = Node[void](label:"a") - b = Node[void](label:"b") - c = Node[void](label:"c") - d = Node[void](label:"d") - e = Node[void](label:"e") - f = Node[void](label:"f") - g = Node[void](label:"g") - tree = Tree[void](root: a, rooted: true) - -a.add_child(b) -a.add_child(c) -c.add_child(d) -c.add_child(e) -e.add_child(f) -e.add_child(g) - -template toSeq(iter: untyped): untyped = - var s: seq[string] - for i in iter: - s.add(i.label) - s.join(" ") - -suite "Tree Type": - test "preorder": - check(toSeq(tree.preorder) == "a b c d e f g") - - test "postorder": - check(toSeq(tree.postorder) == "b d f g e c a") - - test "levelorder": - check(toSeq(tree.levelorder) == "a b c d e f g") - - test "inorder": - check(toSeq(tree.inorder) == "b a d c f e g") - - test "iterleaves": - check(toSeq(tree.iterleaves) == "b d f g") - - test "newickorder": - var newickorder: seq[(string, bool)] - for i in tree.newickorder(): newickorder.add((i.node.label, i.firstVisit)) - check (newickorder == @[("a", true), ("b", true), ("c", true), ("d", true), ("e", true), ("f", true), ("g", true), ("e", false), ("c", false), ("a", false)]) - - test "ladderize": - tree.ladderize() - check(toSeq(tree.preorder) == "a c e f g d b") - tree.ladderize(Descending) - check(toSeq(tree.preorder) == "a b c d e f g") - - test "ascii": - check(tree.ascii == """ - -a /-b - \c /-d - \e /-f - \-g""".dedent()) - - test "prune": - var pruned = tree - pruned.prune(e) - check(toSeq(tree.preorder) == "a b d") - - diff --git a/tests/test_writeNewick.nim b/tests/test_writeNewick.nim deleted file mode 100644 index 2e35295..0000000 --- a/tests/test_writeNewick.nim +++ /dev/null @@ -1,46 +0,0 @@ -import ../src/phylogeni -import unittest - -suite "Newick Writer": - test "void type": - let - a = Node[void](label:"a", length:1.0) - b = Node[void](label:"b", length:1.0) - c = Node[void](label:"c", length:1.0) - d = Node[void](label:"d", length:1.0) - e = Node[void](label:"e", length:1.0) - f = Node[void](label:"f", length:1.0) - g = Node[void](label:"g", length:1.0) - tree = Tree[void](root: a, rooted:true) - a.add_child(b) - a.add_child(c) - c.add_child(d) - c.add_child(e) - e.add_child(f) - e.add_child(g) - var - s = tree.writeNewickString() - expected = "[&R](b:1.0,(d:1.0,(f:1.0,g:1.0)e:1.0)c:1.0)a:1.0;" - check(s == expected) - - - test "string type": - let - a = Node[string](label:"a", length:1.0, data:"data") - b = Node[string](label:"b", length:1.0, data:"data") - c = Node[string](label:"c", length:1.0, data:"data") - d = Node[string](label:"d", length:1.0, data:"data") - e = Node[string](label:"e", length:1.0, data:"data") - f = Node[string](label:"f", length:1.0, data:"data") - g = Node[string](label:"g", length:1.0, data:"data") - tree = Tree[string](root: a, rooted: true) - a.add_child(b) - a.add_child(c) - c.add_child(d) - c.add_child(e) - e.add_child(f) - e.add_child(g) - var - s = tree.writeNewickString() - expected = "[&R](b:1.0[&data],(d:1.0[&data],(f:1.0[&data],g:1.0[&data])e:1.0[&data])c:1.0[&data])a:1.0[&data];" - check(s == expected)