Newer
Older
ecologia / analysis / phylogeny / ecophyl.lisp
;;;;
;;;; ecophyl is a phylogenic analysis tool for Ecologia that keeps track of
;;;; every animal in a run and enables lines of descent to be studied.
;;;; This file contains the main Common Lisp package for ecophyl.
;;;;
;;;; Copyright (c) 2016 Daniel Vedder
;;;; Licensed under the terms of the GPLv3.
;;;;

;;; ECOLOGIA VERSION 1.1

;;; Define the package

(defpackage "ECOPHYL"
	(:use "COMMON-LISP")
	(:export "GET-ANIMAL" "ANALYSE-LOG" "SAVE-ANIMALS" "LOAD-ANIMALS"
		"ANCESTORS" "OFFSPRING" "LCA" "LATEST-COMMON-ANCESTORS"
		"ANIMAL" "GENOME" "*ANIMALS*")) 

(in-package "ECOPHYL")


;;; Define needed variables and structs

(defstruct animal
	(id 0)
	(species NIL)
	(parent 0)
	(generation 0)
	(offspring NIL)
	(born 0)
	(age -1)
	(genome NIL))

(defstruct genome
	(mutation-rate 0)
	(speed 0)
	(stamina 0)
	(sight 0)
	(metabolism 0)
	(age-limit 0)
	(strength 0)
	(reproductive-energy 0)
	(maturity-age 0)
	(gestation 0)
	(reproduction-rate 0))

;;Create the list of animals with a generic "ancestor"
(defvar *animals* (list (make-animal)))


;;; Ecologia-related functions
;; I/O functions

(defun get-animal (id)
	"Return the animal with this ID number"
	(dolist (a *animals*)
		(when (= (animal-id a) id)
			(return a))))

(defun analyse-log (logfile)
	"Read in a log file and extract all animal data"
	;;XXX This involves a lot of very precise string surgery.
	;; Any change in the log format (as regards analysis() calls) is
	;; likely to lead to breakage here!
	;;FIXME Try to remove the remaining 'surgery' bits
	(do* ((log (load-text-file logfile)) (ln (length log))
			 (i 0 (1+ i)) (line (nth i log) (if (= i ln) "" (nth i log)))
			 (words (split-string line #\space) (split-string line #\space)))
		((= i ln) (format t "~&Done."))
		(when (member "ANALYSIS:" words :test #'equalp)
			(format t "~&Analysing line ~S." i)
			(cond ((member "created" words :test #'equalp)
					  (let* ((parent (get-animal (get-property "parent" words)))
								(id (get-property "ID"))
								(a (make-animal :id id
									   :species (nth 5 words)
									   :parent (animal-id parent)
									   :generation (get-property "generation")
									   :born (get-property "update"))))
						  (setf *animals* (append *animals* (list a)))
						  (setf (animal-offspring parent)
							  (append (animal-offspring parent) (list id)))))
				((member "genome" words :test #'equalp)
					(let ((id (read-from-string
								  (first (cut-string (nth 7 words)
											 (1- (length (nth 7 words)))))))
							 (g (make-genome
									:age-limit (get-property "ageLimit" words)
									:maturity-age (get-property "maturityAge")
									:strength (get-property "strength")
									:reproductive-energy (get-property "reproductiveEnergy")
									:stamina (get-property "stamina")
									:sight (get-property "sight")
									:mutation-rate (get-property "mutationRate")
									:metabolism (get-property "metabolism")
									:reproduction-rate (get-property "reproductionRate")
									:gestation (get-property "gestation")
									:speed (get-property "speed"))))
						(setf (animal-genome (get-animal id)) g)))
				((member "died" words :test #'equalp)
					(let ((id (read-from-string (nth 5 words)))
							 (age (read-from-string (nth 9 words))))
						(setf (animal-age (get-animal id)) age)))))))

(let ((plist))
	(defun get-property (prop &optional prop-list)
		"Extract the value of prop from a list of strings of the form 'x=y'"
		;; A helper function for analyse-log
		;; The property list is cached for easier syntax/better performance
		(if prop-list (setf plist prop-list)
			(unless plist (error "get-property: No property list specified!")))
		(dolist (elt plist)
			(let ((pv (split-string elt #\=)))
				(when (equalp prop (first pv))
					(return-from get-property (read-from-string (second pv))))))))

(defun save-animals (filename)
	"Save a list of animals to file"
	(with-open-file (f filename :direction :output)
		(format f "~S" *animals*)))

(defun load-animals (filename)
	"Load a list of animals previously saved to file"
	(with-open-file (f filename) ;;XXX What if the file doesn't exist?
		(setf *animals* (read f))))

;; Analysis functions

(defun ancestors (id)
	"Find the ancestors of the given animal"
	(if (zerop id) NIL
		(cons id (ancestors (animal-parent (get-animal id))))))

(defun offspring (id)
	"Find all offspring of the given animal"
	(let* ((animal (get-animal id)) (offspring (animal-offspring animal)))
		(dolist (c (animal-offspring animal) offspring)
			;;XXX This doesn't order the list, but who cares
			(setf offspring (append offspring (offspring c))))))

;; XXX This is *very* time intensive!
(defun latest-common-ancestors (alist)
	"Find the latest common ancestors of a list of animals"
	;; More specifically, return the shortest list of animal IDs who
	;; together are ancestral to all inputed animals
	(let ((mca (lca alist)) (next-list nil))
		(setf next-list
			(remove-if #'(lambda (a) (member mca (ancestors a))) alist))
		(if (null next-list) (list mca)
			(cons mca (latest-common-ancestors next-list)))))

(defun lca (alist)
	"Find the most frequent latest common ancestor of a list of animal IDs"
	;; This returns multiple values: the ID of the LCA and its frequency
	(do ((ancestor-list (mapcar #'(lambda (x) (reverse (ancestors x))) alist))
			(mca -1) (freq 0) (next-mca -1) (next-freq 0) (i 0 (1+ i)))
		((or (null next-mca) (< next-freq freq)) (values mca freq))
		(multiple-value-setq (next-mca next-freq)
			(most-common-element (nths i ancestor-list)))
		(when (and (> next-freq freq) next-mca)
			(setf mca next-mca freq next-freq))))

;;; Utility functions

;; XXX Not all of these are going to be needed, I just copied them en bloc
;; from my standard util.lisp file...

;; XXX Copy the whole util.lisp file here and use it as its own package?

(defmacro cassoc (entry table &key (test #'eql))
	"Returns (car (cdr (assoc entry table)))"
	`(car (cdr (assoc ,entry ,table :test ,test))))
				
(defun load-text-file (file-name)
	"Load a text file into a list of strings (representing the lines)"
	(with-open-file (f file-name)
		(do* ((line (read-line f nil nil)
				  (read-line f nil nil))
				 (file-lines (list line) (append file-lines (list line))))
			((null line) file-lines))))

(defun string-from-list (lst &optional (separator " - "))
	"Put all elements of lst into a single string, separated by the separator"
	(cond ((null lst) "")
		((= (length lst) 1) (to-string (car lst)))
		(T (concatenate 'string (to-string (first lst)) (to-string separator)
			(string-from-list (cdr lst) separator)))))

(defun split-string (str separator)
	"Split the string up into a list of strings along the separator character"
	(cond ((equalp str (to-string separator)) NIL)
		((zerop (count-instances separator str)) (list str))
		(T (let ((split-elt (cut-string str (position separator str))))
			   (cons (first split-elt)
				   (split-string (second (cut-string (second split-elt) 1))
					   separator))))))

(defun cut-string (s i)
	"Cut string s in two at index i and return the two substrings in a list"
	(if (or (minusp i) (> i (length s))) s
		(let ((s1 (make-string i)) (s2 (make-string (- (length s) i))))
			(dotimes (c (length s) (list s1 s2))
				(if (> i c)
					(setf (aref s1 c) (aref s c))
					(setf (aref s2 (- c i)) (aref s c)))))))

(defun char-list-to-string (char-list)
	"Convert a character list to a string"
	(let ((s (make-string (length char-list) :initial-element #\SPACE)))
		(dotimes (i (length char-list) s)
			(setf (aref s i) (nth i char-list)))))

(defun trim-whitespace (s)
	"Trim off spaces and tabs before and after string s"
	(string-trim '(#\space #\tab) s))

(defun to-string (x)
	"Whatever x is, convert it into a string"
	(cond ((stringp x) x)
		((or (symbolp x) (characterp x)) (string x))
		(t (format NIL "~S" x))))

(defun extract-elements (str)
	"Extract all Lisp elements (strings, symbols, numbers, etc.) from str"
	(multiple-value-bind (next-element i) (read-from-string str nil)
		(if (null next-element) NIL
			(cons next-element
				(extract-elements (second (cut-string str i)))))))

(defun count-instances (search-term search-sequence &key (test #'eql))
	"Count the number of instances of search-term in search-sequence"
	(let ((count 0))
		(dotimes (i (length search-sequence) count)
			(when (funcall test search-term (elt search-sequence i))
				(incf count)))))

(defun most-common-element (lst &key (test #'eql))
	"Return the most common element in this list and how often it appears"
	;;This function has multiple return values!
	;;In case of multiple mces, return the one that appears first
	(let ((elements-counted NIL) (max 0) (mce NIL))
		(dolist (e lst (values mce max))
			(unless (member e elements-counted :test test)
				(let ((count (count-instances e lst :test test)))
					(when (> count max)
						(setf max count)
						(setf mce e)))
				(setf elements-counted (append elements-counted (list e)))))))

(defun nths (n lst)
	"Take in a list of lists and return the nth element of each"
	(when (and lst (listp (car lst)))
		(cons (nth n (car lst)) (nths n (cdr lst)))))

(defun range (stop &key (start 0) (step 1))
	"Return a list of numbers from start to stop"
	;;XXX Surely this must exist as a function in Common Lisp already,
	;; I just don't know what it's called...
	(unless (>= start stop)
		(cons start (range stop :start (+ start step) :step step))))
		
;;; For acceptable performance during analysis,
;;; some functions need to be compiled
(dolist (fn '(analyse-log get-property split-string
				 cut-string count-instances))
	(compile fn))