From: tla Date: Wed, 14 Aug 2013 14:20:33 +0000 (+0200) Subject: initial commit of IDP scripts for stemma analysis X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=516509661a4edaad45bacd27fbb5ce0f59417451;p=scpubgit%2Fstemmatology.git initial commit of IDP scripts for stemma analysis --- diff --git a/analysis/idp_server/findClasses.idp b/analysis/idp_server/findClasses.idp new file mode 100644 index 0000000..ff7c15a --- /dev/null +++ b/analysis/idp_server/findClasses.idp @@ -0,0 +1,78 @@ +vocabulary V3 { + extern vocabulary V + type Class + ClassOf(Manuscript): Class + Copy: Class + Revert: Class + Source: Class + IndirectAncestor(Manuscript,Manuscript) + type N isa nat + NbOfSources : N + NbOfReverts : N +} + +theory T3 : V3 { + ! x : (ClassOf(x)=Copy) <=> ? y : CopiedBy(y,x) & VariantOf(y) = VariantOf(x). + ! x : (ClassOf(x)=Revert) <=> (ClassOf(x)~=Copy) & ? y : IndirectAncestor(y,x) & VariantOf(y) = VariantOf(x). + + { !x y: IndirectAncestor(x,y) <- ?z: CopiedBy(x,z) & IndirectAncestor(z,y). + !x y: IndirectAncestor(x,y) <- ?z: CopiedBy(x,z) & CopiedBy(z,y). } + + NbOfSources = #{x: ClassOf(x)=Source}. + NbOfReverts = #{x: ClassOf(x)=Revert}. +} + +term TotalCost : V3 { + 3 * NbOfSources + NbOfReverts +} + +procedure minSourcesAndReverts(sample) { + stdoptions.groundwithbounds=false + stdoptions.symmetrybreaking='static' + stdoptions.cpsupport=true + addClasses(sample) + return minimize(T3, sample, TotalCost)[1] +} + +structure SClass : V3 { + Class = { copy; revert; source } + Copy = copy + Revert = revert + Source = source +} + +procedure addClasses(struct) { + idpintern.setvocabulary(struct,V3) + struct[V3::Class.type] = SClass[V3::Class.type] + struct[V3::Copy] = SClass[V3::Copy] + struct[V3::Revert] = SClass[V3::Revert] + struct[V3::Source] = SClass[V3::Source] + struct[V3::N.type] = range(0,#totable(struct[V::Manuscript.type])) +} + +procedure findClasses(samples) { + local result = {} + for _,sample in ipairs(samples) do + local model = minSourcesAndReverts(sample) + if model then + table.insert(result,{ getGrouping(model), getClasses(model) }) + else + table.insert(result,{ getGrouping(sample), {} }) + end + end + return result +} + +procedure getClasses(model) { + local classes = {} + for tuple in tuples(model[V3::ClassOf].graph.ct) do + local manuscript, class = tuple[1], tuple[2] + if classes[class] == nil then + classes[class] = {manuscript} + else + table.insert(classes[class],manuscript) + end + end + return classes +} + diff --git a/analysis/idp_server/findGroupings.idp b/analysis/idp_server/findGroupings.idp new file mode 100644 index 0000000..7497164 --- /dev/null +++ b/analysis/idp_server/findGroupings.idp @@ -0,0 +1,42 @@ +vocabulary V1 { + extern vocabulary V + SourceOf(Variant): Manuscript +} + +theory T1 : V1 { + ! x : (x ~= SourceOf(VariantOf(x))) => ? y : CopiedBy(y,x) & VariantOf(y) = VariantOf(x). +} + +procedure check(sample) { + stdoptions.groundwithbounds=false + stdoptions.symmetrybreaking='static' + idpintern.setvocabulary(sample,V1) + return onemodel(T1,sample) +} + +procedure findGroupings(samples) { + local result = {} + for _,sample in ipairs(samples) do + -- Find out whether the sample fits the stemma. + local model = check(sample) + if model then + table.insert(result,{ getGrouping(model), true }) + else + table.insert(result,{ getGrouping(sample), false }) + end + end + return result +} + +procedure getGrouping(model) { + local grouping = {} + for tuple in tuples(model[V::VariantOf].graph.ct) do + local manuscript, variant = tuple[1], tuple[2] + if grouping[variant] == nil then + grouping[variant] = {manuscript} + else + table.insert(grouping[variant],manuscript) + end + end + return grouping +} diff --git a/analysis/idp_server/findSources.idp b/analysis/idp_server/findSources.idp new file mode 100644 index 0000000..0b9630d --- /dev/null +++ b/analysis/idp_server/findSources.idp @@ -0,0 +1,43 @@ +vocabulary V2 { + extern vocabulary V + IsSource(Manuscript) +} + +theory T2 : V2 { + ! x : (~IsSource(x)) => ? y : CopiedBy(y,x) & VariantOf(y) = VariantOf(x). +} + +term NbOfSources : V2 { + #{ x : IsSource(x) } +} + +procedure minSources(sample) { + stdoptions.groundwithbounds=false + stdoptions.symmetrybreaking='static' + idpintern.setvocabulary(sample,V2) + return minimize(T2, sample, NbOfSources)[1] +} + +procedure findSources(samples) { + local result = {} + for _,sample in ipairs(samples) do + local model = minSources(sample) + if model then + table.insert(result,{ getGrouping(model), getSources(model) }) + else + table.insert(result,{ getGrouping(sample), {} }) + end + end + return result +} + +procedure getSources(model) { + local sources = {} + for tuple in tuples(model[V2::IsSource].ct) do + local manuscript = tuple[1] + table.insert(sources,manuscript) + end + return sources +} + + diff --git a/analysis/idp_server/json.lua b/analysis/idp_server/json.lua new file mode 100755 index 0000000..6c54130 --- /dev/null +++ b/analysis/idp_server/json.lua @@ -0,0 +1,521 @@ +----------------------------------------------------------------------------- +-- JSON4Lua: JSON encoding / decoding support for the Lua language. +-- json Module. +-- Author: Craig Mason-Jones +-- Homepage: http://json.luaforge.net/ +-- Version: 0.9.50 +-- This module is released under the MIT License (MIT). +-- Please see LICENCE.txt for details. +-- +-- USAGE: +-- This module exposes two functions: +-- encode(o) +-- Returns the table / string / boolean / number / nil / json.null value as a JSON-encoded string. +-- decode(json_string) +-- Returns a Lua object populated with the data encoded in the JSON string json_string. +-- +-- REQUIREMENTS: +-- compat-5.1 if using Lua 5.0 +-- +-- CHANGELOG +-- 0.9.50 Radical performance improvement on decode from Eike Decker. Many thanks! +-- 0.9.40 Changed licence to MIT License (MIT) +-- 0.9.20 Introduction of local Lua functions for private functions (removed _ function prefix). +-- Fixed Lua 5.1 compatibility issues. +-- Introduced json.null to have null values in associative arrays. +-- encode() performance improvement (more than 50%) through table.concat rather than .. +-- Introduced decode ability to ignore /**/ comments in the JSON string. +-- 0.9.10 Fix to array encoding / decoding to correctly manage nil/null values in arrays. +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Imports and dependencies +----------------------------------------------------------------------------- +local math = require('math') +local string = require("string") +local table = require("table") +local tostring = tostring + +local base = _G + +----------------------------------------------------------------------------- +-- Module declaration +----------------------------------------------------------------------------- +module("json") + +-- Public functions + +-- Private functions +local decode_scanArray +local decode_scanComment +local decode_scanConstant +local decode_scanNumber +local decode_scanObject +local decode_scanString +local decode_scanWhitespace +local encodeString +local isArray +local isEncodable + +----------------------------------------------------------------------------- +-- PUBLIC FUNCTIONS +----------------------------------------------------------------------------- +--- Encodes an arbitrary Lua object / variable. +-- @param v The Lua object / variable to be JSON encoded. +-- @return String containing the JSON encoding in internal Lua string format (i.e. not unicode) +function encode (v) + -- Handle nil values + if v==nil then + return "null" + end + + local vtype = base.type(v) + + -- Handle strings + if vtype=='string' then + return '"' .. encodeString(v) .. '"' -- Need to handle encoding in string + end + + -- Handle booleans + if vtype=='number' or vtype=='boolean' then + return base.tostring(v) + end + + -- Handle tables + if vtype=='table' then + local rval = {} + -- Consider arrays separately + local bArray, maxCount = isArray(v) + if bArray then + for i = 1,maxCount do + table.insert(rval, encode(v[i])) + end + else -- An object, not an array + for i,j in base.pairs(v) do + if isEncodable(i) and isEncodable(j) then + table.insert(rval, '"' .. encodeString(i) .. '":' .. encode(j)) + end + end + end + if bArray then + return '[' .. table.concat(rval,',') ..']' + else + return '{' .. table.concat(rval,',') .. '}' + end + end + + -- Handle null values + if vtype=='function' and v==null then + return 'null' + end + + base.assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. base.tostring(v)) +end + + +--- Decodes a JSON string and returns the decoded value as a Lua data structure / value. +-- @param s The string to scan. +-- @return Lua objectthat was scanned, as a Lua table / string / number / boolean or nil. +function decode(s) + -- Function is re-defined below after token and other items are created. + -- Just defined here for code neatness. + return null +end + +--- The null function allows one to specify a null value in an associative array (which is otherwise +-- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null } +function null() + return null -- so json.null() will also return null ;-) +end + +----------------------------------------------------------------------------- +-- Internal, PRIVATE functions. +----------------------------------------------------------------------------- + +--- Encodes a string to be JSON-compatible. +-- This just involves back-quoting inverted commas, back-quotes and newlines, I think ;-) +-- @param s The string to return as a JSON encoded (i.e. backquoted string) +-- @return The string appropriately escaped. +local qrep = {["\\"]="\\\\", ['"']='\\"',['\n']='\\n',['\t']='\\t'} +function encodeString(s) + return tostring(s):gsub('["\\\n\t]',qrep) +end + +-- Determines whether the given Lua type is an array or a table / dictionary. +-- We consider any table an array if it has indexes 1..n for its n items, and no +-- other data in the table. +-- I think this method is currently a little 'flaky', but can't think of a good way around it yet... +-- @param t The table to evaluate as an array +-- @return boolean, number True if the table can be represented as an array, false otherwise. If true, +-- the second returned value is the maximum +-- number of indexed elements in the array. +function isArray(t) + -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable + -- (with the possible exception of 'n') + local maxIndex = 0 + for k,v in base.pairs(t) do + if (base.type(k)=='number' and math.floor(k)==k and 1<=k) then -- k,v is an indexed pair + if (not isEncodable(v)) then return false end -- All array elements must be encodable + maxIndex = math.max(maxIndex,k) + else + if (k=='n') then + if v ~= table.getn(t) then return false end -- False if n does not hold the number of elements + else -- Else of (k=='n') + if isEncodable(v) then return false end + end -- End of (k~='n') + end -- End of k,v not an indexed pair + end -- End of loop across all pairs + return true, maxIndex +end + +--- Determines whether the given Lua object / table / variable can be JSON encoded. The only +-- types that are JSON encodable are: string, boolean, number, nil, table and json.null. +-- In this implementation, all other types are ignored. +-- @param o The object to examine. +-- @return boolean True if the object should be JSON encoded, false if it should be ignored. +function isEncodable(o) + local t = base.type(o) + return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null) +end + +-- Radical performance improvement for decode from Eike Decker! +do + local type = base.type + local error = base.error + local assert = base.assert + local print = base.print + local tonumber = base.tonumber + -- initialize some values to be used in decoding function + + -- initializes a table to contain a byte=>table mapping + -- the table contains tokens (byte values) as keys and maps them on other + -- token tables (mostly, the boolean value 'true' is used to indicate termination + -- of a token sequence) + -- the token table's purpose is, that it allows scanning a sequence of bytes + -- until something interesting has been found (e.g. a token that is not expected) + -- name is a descriptor for the table to be printed in error messages + local function init_token_table (tt) + local struct = {} + local value + function struct:link(other_tt) + value = other_tt + return struct + end + function struct:to(chars) + for i=1,#chars do + tt[chars:byte(i)] = value + end + return struct + end + return function (name) + tt.name = name + return struct + end + end + + -- keep "named" byte values at hands + local + c_esc, + c_e, + c_l, + c_r, + c_u, + c_f, + c_a, + c_s, + c_slash = ("\\elrufas/"):byte(1,9) + + -- token tables - tt_doublequote_string = strDoubleQuot, tt_singlequote_string = strSingleQuot + local + tt_object_key, + tt_object_colon, + tt_object_value, + tt_doublequote_string, + tt_singlequote_string, + tt_array_value, + tt_array_seperator, + tt_numeric, + tt_boolean, + tt_null, + tt_comment_start, + tt_comment_middle, + tt_ignore --< tt_ignore is special - marked tokens will be tt_ignored + = {},{},{},{},{},{},{},{},{},{},{},{},{} + + -- strings to be used in certain token tables + local strchars = "" -- all valid string characters (all except newlines) + local allchars = "" -- all characters that are valid in comments + --local escapechar = {} + for i=0,0xff do + local c = string.char(i) + if c~="\n" and c~="\r" then strchars = strchars .. c end + allchars = allchars .. c + --escapechar[i] = "\\" .. string.char(i) + end + +--[[ + charstounescape = "\"\'\\bfnrt/"; + unescapechars = "\"'\\\b\f\n\r\t\/"; + for i=1,#charstounescape do + escapechar[ charstounescape:byte(i) ] = unescapechars:sub(i,i) + end +]]-- + + -- obj key reader, expects the end of the object or a quoted string as key + init_token_table (tt_object_key) "object (' or \" or } or , expected)" + :link(tt_singlequote_string) :to "'" + :link(tt_doublequote_string) :to '"' + :link(true) :to "}" + :link(tt_object_key) :to "," + :link(tt_comment_start) :to "/" + :link(tt_ignore) :to " \t\r\n" + + + -- after the key, a colon is expected (or comment) + init_token_table (tt_object_colon) "object (: expected)" + :link(tt_object_value) :to ":" + :link(tt_comment_start) :to "/" + :link(tt_ignore) :to" \t\r\n" + + -- as values, anything is possible, numbers, arrays, objects, boolean, null, strings + init_token_table (tt_object_value) "object ({ or [ or ' or \" or number or boolean or null expected)" + :link(tt_object_key) :to "{" + :link(tt_array_seperator) :to "[" + :link(tt_singlequote_string) :to "'" + :link(tt_doublequote_string) :to '"' + :link(tt_numeric) :to "0123456789.-" + :link(tt_boolean) :to "tf" + :link(tt_null) :to "n" + :link(tt_comment_start) :to "/" + :link(tt_ignore) :to " \t\r\n" + + -- token tables for reading strings + init_token_table (tt_doublequote_string) "double quoted string" + :link(tt_ignore) :to (strchars) + :link(c_esc) :to "\\" + :link(true) :to '"' + + init_token_table (tt_singlequote_string) "single quoted string" + :link(tt_ignore) :to (strchars) + :link(c_esc) :to "\\" + :link(true) :to "'" + + -- array reader that expects termination of the array or a comma that indicates the next value + init_token_table (tt_array_value) "array (, or ] expected)" + :link(tt_array_seperator) :to "," + :link(true) :to "]" + :link(tt_comment_start) :to "/" + :link(tt_ignore) :to " \t\r\n" + + -- a value, pretty similar to tt_object_value + init_token_table (tt_array_seperator) "array ({ or [ or ' or \" or number or boolean or null expected)" + :link(tt_object_key) :to "{" + :link(tt_array_seperator) :to "[" + :link(tt_singlequote_string) :to "'" + :link(tt_doublequote_string) :to '"' + :link(tt_comment_start) :to "/" + :link(tt_numeric) :to "0123456789.-" + :link(tt_boolean) :to "tf" + :link(tt_null) :to "n" + :link(tt_ignore) :to " \t\r\n" + + -- valid number tokens + init_token_table (tt_numeric) "number" + :link(tt_ignore) :to "0123456789.-Ee" + + -- once a comment has been started with /, a * is expected + init_token_table (tt_comment_start) "comment start (* expected)" + :link(tt_comment_middle) :to "*" + + -- now everything is allowed, watch out for * though. The next char is then checked manually + init_token_table (tt_comment_middle) "comment end" + :link(tt_ignore) :to (allchars) + :link(true) :to "*" + + function decode (js_string) + local pos = 1 -- position in the string + + -- read the next byte value + local function next_byte () pos = pos + 1 return js_string:byte(pos-1) end + + -- in case of error, report the location using line numbers + local function location () + local n = ("\n"):byte() + local line,lpos = 1,0 + for i=1,pos do + if js_string:byte(i) == n then + line,lpos = line + 1,1 + else + lpos = lpos + 1 + end + end + return "Line "..line.." character "..lpos + end + + -- debug func + --local function status (str) + -- print(str.." ("..s:sub(math.max(1,p-10),p+10)..")") + --end + + -- read the next token, according to the passed token table + local function next_token (tok) + while pos <= #js_string do + local b = js_string:byte(pos) + local t = tok[b] + if not t then + error("Unexpected character at "..location()..": ".. + string.char(b).." ("..b..") when reading "..tok.name.."\nContext: \n".. + js_string:sub(math.max(1,pos-30),pos+30).."\n"..(" "):rep(pos+math.min(-1,30-pos)).."^") + end + pos = pos + 1 + if t~=tt_ignore then return t end + end + error("unexpected termination of JSON while looking for "..tok.name) + end + + -- read a string, double and single quoted ones + local function read_string (tok) + local start = pos + --local returnString = {} + repeat + local t = next_token(tok) + if t == c_esc then + --table.insert(returnString, js_string:sub(start, pos-2)) + --table.insert(returnString, escapechar[ js_string:byte(pos) ]) + pos = pos + 1 + --start = pos + end -- jump over escaped chars, no matter what + until t == true + return (base.loadstring("return " .. js_string:sub(start-1, pos-1) ) ()) + + -- We consider the situation where no escaped chars were encountered separately, + -- and use the fastest possible return in this case. + + --if 0 == #returnString then + -- return js_string:sub(start,pos-2) + --else + -- table.insert(returnString, js_string:sub(start,pos-2)) + -- return table.concat(returnString,""); + --end + --return js_string:sub(start,pos-2) + end + + local function read_num () + local start = pos + while pos <= #js_string do + local b = js_string:byte(pos) + if not tt_numeric[b] then break end + pos = pos + 1 + end + return tonumber(js_string:sub(start-1,pos-1)) + end + + -- read_bool and read_null are both making an assumption that I have not tested: + -- I would expect that the string extraction is more expensive than actually + -- making manual comparision of the byte values + local function read_bool () + pos = pos + 3 + local a,b,c,d = js_string:byte(pos-3,pos) + if a == c_r and b == c_u and c == c_e then return true end + pos = pos + 1 + if a ~= c_a or b ~= c_l or c ~= c_s or d ~= c_e then + error("Invalid boolean: "..js_string:sub(math.max(1,pos-5),pos+5)) + end + return false + end + + -- same as read_bool: only last + local function read_null () + pos = pos + 3 + local u,l1,l2 = js_string:byte(pos-3,pos-1) + if u == c_u and l1 == c_l and l2 == c_l then return nil end + error("Invalid value (expected null):"..js_string:sub(pos-4,pos-1).. + " ("..js_string:byte(pos-1).."="..js_string:sub(pos-1,pos-1).." / "..c_l..")") + end + + local read_object_value,read_object_key,read_array,read_value,read_comment + + -- read a value depending on what token was returned, might require info what was used (in case of comments) + function read_value (t,fromt) + if t == tt_object_key then return read_object_key({}) end + if t == tt_array_seperator then return read_array({}) end + if t == tt_singlequote_string or + t == tt_doublequote_string then return read_string(t) end + if t == tt_numeric then return read_num() end + if t == tt_boolean then return read_bool() end + if t == tt_null then return read_null() end + if t == tt_comment_start then return read_value(read_comment(fromt)) end + error("unexpected termination - "..js_string:sub(math.max(1,pos-10),pos+10)) + end + + -- read comments until something noncomment like surfaces, using the token reader which was + -- used when stumbling over this comment + function read_comment (fromt) + while true do + next_token(tt_comment_start) + while true do + local t = next_token(tt_comment_middle) + if next_byte() == c_slash then + local t = next_token(fromt) + if t~= tt_comment_start then return t end + break + end + end + end + end + + -- read arrays, empty array expected as o arg + function read_array (o,i) + --if not i then status "arr open" end + i = i or 1 + -- loop until ... + while true do + o[i] = read_value(next_token(tt_array_seperator),tt_array_seperator) + local t = next_token(tt_array_value) + if t == tt_comment_start then + t = read_comment(tt_array_value) + end + if t == true then -- ... we found a terminator token + --status "arr close" + return o + end + i = i + 1 + end + end + + -- object value reading + function read_object_value (o) + local t = next_token(tt_object_value) + return read_value(t,tt_object_value) + end + + -- object key reading, might also terminate the object + function read_object_key (o) + while true do + local t = next_token(tt_object_key) + if t == tt_comment_start then + t = read_comment(tt_object_key) + end + if t == true then return o end + if t == tt_object_key then return read_object_key(o) end + local k = read_string(t) + + if next_token(tt_object_colon) == tt_comment_start then + t = read_comment(tt_object_colon) + end + + local v = read_object_value(o) + o[k] = v + end + end + + -- now let's read data from our string and pretend it's an object value + local r = read_object_value() + if pos<=#js_string then + -- not sure about what to do with dangling characters + --error("Dangling characters in JSON code ("..location()..")") + end + + return r + end +end diff --git a/analysis/idp_server/main.idp b/analysis/idp_server/main.idp new file mode 100644 index 0000000..22dcb1b --- /dev/null +++ b/analysis/idp_server/main.idp @@ -0,0 +1,53 @@ +#include +#include + +procedure main() { + exec(findGroupings) -- findGroupings | findSources | findClasses +} + +procedure exec(task) { + input = io.read("*all") + output = process(input,task) + print(output) +} + +/* ---------- Base vocabulary --------------------------------------------- */ +vocabulary V { + type Manuscript + type Variant + CopiedBy(Manuscript,Manuscript) + VariantOf(Manuscript): Variant +} + +/* ---------- Three tasks ------------------------------------------------- */ + +#include "findGroupings.idp" +#include "findSources.idp" +#include "findClasses.idp" + +/* ---------- Procedures for processing ----------------------------------- */ +#include "readData.idp" + +/** Process input in json format **/ +procedure process(input,task) { + require('json') + local input = json.decode(input) + local stemma = readStemma(input.graph) + local samples = readSamples(stemma,input.groupings) + local result = task(samples) + return json.encode(result) +} + +/** Process input from two files **/ +procedure processFiles(stemmafile,samplefile,task) { + require('json') + local f = assert(io.open(stemmafile,"r")) + local stemma = readStemma(f:read("*all")) + f:close() + local f = assert(io.open(samplefile,"r")) + local samples = readSamples(stemma,json.decode(f:read("*all"))) + f:close() + local result = task(samples) + return json.encode(result) +} + diff --git a/analysis/idp_server/readData.idp b/analysis/idp_server/readData.idp new file mode 100644 index 0000000..a522474 --- /dev/null +++ b/analysis/idp_server/readData.idp @@ -0,0 +1,48 @@ +/** Create structure with stemma data, read from dot string **/ +procedure readStemma(stemmastring) { + local stemma = newstructure(V,"stemma") + local pipe = assert(io.popen("echo '"..stemmastring.."' | dot -Tplain")) + local s = pipe:read("*all") + pipe:close() +-- Add nodes to the stemma. + local t = {} + for node in string.gmatch(s,"node%s(%S+)%s") do + table.insert(t,node) + end + stemma[V::Manuscript.type] = t +-- Add edges to the stemma. + for n1,n2 in string.gmatch(s,"edge%s(%S+)%s(%S+)%s") do + maketrue(stemma[V::CopiedBy],{n1,n2}) + end +-- There are no more edges than the ones given. + stemma[V::CopiedBy].cf = stemma[V::CopiedBy].pf + return stemma +} + +/** Create table of structures with sample data **/ +procedure readSamples(stemma,sampletable) { + local samples = {} + for i,groupings in ipairs(sampletable) do + local sample = newstructure(V,"sample"..tostring(i)) + sample[V::Manuscript.type] = stemma[V::Manuscript.type] + sample[V::CopiedBy] = stemma[V::CopiedBy] + sample[V::Variant.type] = range(1,#groupings) + -- Add known equivalence relations to the sample. + for variant,grouping in ipairs(groupings) do + for _,element in ipairs(grouping) do + maketrue(sample[V::VariantOf].graph,{element,variant}) + end + end + table.insert(samples,sample) + end + return samples +} + +/** Show stemma using dot **/ +procedure showStemma(stemmastring) { + local f = assert(io.open("data/test.dot","w")) + f:write(stemmastring) + f:close() + os.execute("dotty data/test.dot &") +} +