Index: framspy/FramsticksLib.py
===================================================================
--- framspy/FramsticksLib.py	(revision 1078)
+++ framspy/FramsticksLib.py	(revision 1078)
@@ -0,0 +1,189 @@
+from typing import List  # to be able to specify a type hint of list(something)
+import json
+import sys, os
+import argparse
+import numpy as np
+import frams
+
+
+class FramsticksLib:
+	"""Communicates directly with Framsticks DLL/SO.
+	You can perform basic operations like mutation, crossover, and evaluation of genotypes.
+	This way you can perform evolution controlled by python as well as access and manipulate genotypes.
+	You can even design and use in evolution your own genetic representation implemented entirely in python.
+
+	You need to provide one or two parameters when you run this class: the path to Framsticks CLI where .dll/.so resides
+	and the name of the Framsticks dll/so (if it is non-standard). See::
+		FramsticksLib.py -h"""
+
+	DETERMINISTIC: bool = False  # set to True to have the same results in each run
+
+	GENOTYPE_INVALID = "/*invalid*/"  # this is how genotype invalidity is represented in Framsticks
+	EVALUATION_SETTINGS_FILE = "eval-allcriteria.sim"  # MUST be compatible with standard-eval expdef
+
+
+	def __init__(self, framspath, framsexe, pid=""):
+		self.pid = pid if pid is not None else ""
+		self.frams_path = framspath
+		self.frams_exe = framsexe if framsexe is not None else 'frams.exe' if os.name == "nt" else 'frams.linux'
+		self.writing_path = None
+		mainpath = os.path.join(self.frams_path, self.frams_exe)
+		# exe_call = [mainpath, '-Q', '-s', '-c', '-icliutils.ini']  # -c will be ignored in Windows Framsticks (this option is meaningless because the Windows version does not support color console, so no need to deactivate this feature using -c)
+		# exe_call_to_get_version = [mainpath, '-V']
+		# exe_call_to_get_path = [mainpath, '-?']
+
+		frams.init(framspath, "-Ddata")  # "-D"+os.path.join(framspath,"data")) # not possible (maybe python windows issue) because of the need for os.chdir(). So we assume "data" is where the dll/so is
+
+		print('Available objects:', dir(frams))
+		print()
+
+		self.__spawnFramsticksCLI()
+
+
+	def __spawnFramsticksCLI(self):
+		print('Performing a basic test 1/2... ', end='')
+		simplest = self.getSimplest("1")
+		assert simplest == "X" and type(simplest) is str
+		print('OK.')
+		print('Performing a basic test 2/2... ', end='')
+		assert self.isValid(["X[0:0],", "X[0:0]", "X[1:0]"]) == [False, True, False]
+		print('OK.')
+		if not self.DETERMINISTIC:
+			frams.Math.randomize();
+		frams.Simulator.expdef = "standard-eval"  # this expdef must be used by EVALUATION_SETTINGS_FILE
+
+
+	def getSimplest(self, genetic_format) -> str:
+		return frams.GenMan.getSimplest(genetic_format).genotype._string()
+
+
+	def evaluate(self, genotype_list: List[str]):
+		"""
+		Returns:
+			List of dictionaries containing the performance of genotypes evaluated using self.EVALUATION_SETTINGS_FILE.
+			Note that for whatever reason (e.g. incorrect genotype), the dictionaries you will get may be empty or
+			partially empty and may not have the fields you expected, so handle such cases properly.
+		"""
+		assert isinstance(genotype_list, list)  # because in python str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
+
+		# TODO use Logger to mute stdout (optinally just like in FramsticksCLI.py)
+		frams.GenePools[0].clear()
+		frams.Simulator.ximport(self.EVALUATION_SETTINGS_FILE, 2 + 4 + 8 + 16)
+		for g in genotype_list:
+			frams.GenePools[0].add(g)
+		frams.ExpProperties.evalsavefile = ""  # no need to store results in a file - we will get evaluations directly from Genotype's "data" field
+		frams.Simulator.init()
+		frams.Simulator.start()
+		step = frams.Simulator.step  # cache reference to avoid repeated lookup in the loop
+		while frams.Simulator.running._int():  # standard-eval.expdef sets running to 0 when the evaluation is complete
+			step()
+
+		results = []
+		for g in frams.GenePools[0]:
+			serialized_dict = frams.String.serialize(g.data[frams.ExpProperties.evalsavedata._value()])
+			evaluations = json.loads(serialized_dict._string())
+			# now, for consistency with FramsticksCLI.py, add "num" and "name" keys that are missing because we got data directly from Genotype, not from the file produced by standard-eval.expdef's function printStats(). What we do below is what printStats() does.
+			result = {"num": g.num._value(), "name": g.name._value(), "evaluations": evaluations}
+			results.append(result)
+
+		return results
+
+
+	def mutate(self, genotype_list: List[str]) -> List[str]:
+		"""
+		Returns:
+			The genotype(s) of the mutated source genotype(s). self.GENOTYPE_INVALID for genotypes whose mutation failed (for example because the source genotype was invalid).
+		"""
+		assert isinstance(genotype_list, list)  # because in python str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
+
+		mutated = []
+		for g in genotype_list:
+			mutated.append(frams.GenMan.mutate(frams.Geno.newFromString(g)).genotype._string())
+		assert len(genotype_list) == len(mutated), "Submitted %d genotypes, received %d validity values" % (len(genotype_list), len(mutated))
+		return mutated
+
+
+	def crossOver(self, genotype_parent1: str, genotype_parent2: str) -> str:
+		"""
+		Returns:
+			The genotype of the offspring. self.GENOTYPE_INVALID if the crossing over failed.
+		"""
+		return frams.GenMan.crossOver(frams.Geno.newFromString(genotype_parent1), frams.Geno.newFromString(genotype_parent2)).genotype._string()
+
+
+	def dissimilarity(self, genotype_list: List[str]) -> np.ndarray:
+		"""
+		Returns:
+			A square array with dissimilarities of each pair of genotypes.
+		"""
+		assert isinstance(genotype_list, list)  # because in python str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
+
+		frams.SimilMeasure.type = 1  # adjust to your needs. Set here because loading EVALUATION_SETTINGS_FILE during evaluation may overwrite these parameters
+		frams.SimilMeasureHungarian.simil_weightedMDS = 1
+		frams.SimilMeasureHungarian.simil_partgeom = 1
+
+		n = len(genotype_list)
+		square_matrix = np.zeros((n, n))
+		genos = []  # prepare an array of Geno objects so we don't need to convert raw strings to Geno objects all the time
+		for g in genotype_list:
+			genos.append(frams.Geno.newFromString(g))
+		for i in range(n):
+			for j in range(n):  # maybe calculate only one triangle if you really need a 2x speedup
+				square_matrix[i][j] = frams.SimilMeasure.evaluateDistance(genos[i], genos[j])._double()
+
+		for i in range(n):
+			assert square_matrix[i][i] == 0, "Not a correct dissimilarity matrix, diagonal expected to be 0"
+		assert (square_matrix == square_matrix.T).all(), "Probably not a correct dissimilarity matrix, expecting symmetry, verify this"  # could introduce tolerance in comparison (e.g. class field DISSIMIL_DIFF_TOLERANCE=10^-5) so that miniscule differences do not fail here
+		return square_matrix
+
+
+	def isValid(self, genotype_list: List[str]) -> List[bool]:
+		assert isinstance(genotype_list, list)  # because in python str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
+		valid = []
+		for g in genotype_list:
+			valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
+		assert len(genotype_list) == len(valid), "Submitted %d genotypes, received %d validity values" % (len(genotype_list), len(valid))
+		return valid
+
+
+def parseArguments():
+	parser = argparse.ArgumentParser(description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[0])
+	parser.add_argument('-path', type=ensureDir, required=True, help='Path to Framsticks CLI without trailing slash.')
+	parser.add_argument('-exe', required=False, help='Library name. If not given, "frams.dll" or "frams.so" is assumed depending on the platform.')  # TODO dll,so, allow to change name?
+	parser.add_argument('-genformat', required=False, help='Genetic format for the demo run, for example 4, 9, or S. If not given, f1 is assumed.')
+	return parser.parse_args()
+
+
+def ensureDir(string):
+	if os.path.isdir(string):
+		return string
+	else:
+		raise NotADirectoryError(string)
+
+
+if __name__ == "__main__":
+	# A demo run.
+
+	# TODO ideas:
+	# - check_validity with three levels (invalid, corrected, valid)
+	# - a pool of binaries running simultaneously, balance load - in particular evaluation
+
+	parsed_args = parseArguments()
+	framsDLL = FramsticksLib(parsed_args.path, parsed_args.exe, None)  # parsed_args.pid)
+
+	print("Sending a direct command to Framsticks CLI that calculates \"4\"+2 yields", frams.Simulator.eval("return \"4\"+2;"))
+
+	simplest = framsDLL.getSimplest('1' if parsed_args.genformat is None else parsed_args.genformat)
+	print("\tSimplest genotype:", simplest)
+	parent1 = framsDLL.mutate([simplest])[0]
+	parent2 = parent1
+	MUTATE_COUNT = 10
+	for x in range(MUTATE_COUNT):  # example of a chain of 10 mutations
+		parent2 = framsDLL.mutate([parent2])[0]
+	print("\tParent1 (mutated simplest):", parent1)
+	print("\tParent2 (Parent1 mutated %d times):" % MUTATE_COUNT, parent2)
+	offspring = framsDLL.crossOver(parent1, parent2)
+	print("\tCrossover (Offspring):", offspring)
+	print('\tDissimilarity of Parent1 and Offspring:', framsDLL.dissimilarity([parent1, offspring])[0, 1])
+	print('\tPerformance of Offspring:', framsDLL.evaluate([offspring]))
+	print('\tValidity of Parent1, Parent 2, and Offspring:', framsDLL.isValid([parent1, parent2, offspring]))
Index: framspy/frams-test.py
===================================================================
--- framspy/frams-test.py	(revision 1078)
+++ framspy/frams-test.py	(revision 1078)
@@ -0,0 +1,28 @@
+import frams
+
+framspath = "" # set to where Framsticks binaries and libraries exist
+frams.init(framspath, "-Ddata")  # "-D"+os.path.join(framspath,"data")) # not possible (maybe python windows issue) because of the need for os.chdir(). So we assume "data" is where the dll/so is
+
+print('Available objects:', dir(frams))
+print()
+
+
+def extValueDetails(v):
+	return '"' + str(v) + '"    frams type=' + str(v._type()) + '    frams class=' + str(v._class()) + '    python type=' + str(type(v._value()))
+
+
+v = frams.String.deserialize('[100,2.2,"abc",[null,[]],XYZ[9,8,7]]')
+print(extValueDetails(v))
+for e in v:
+	print(extValueDetails(e))
+
+g = frams.GenePools[0].add('X')
+g.name = "py!"
+g.data['custom'] = 123.456
+g.data['a'] = 'b'
+
+for more in range(5):
+	frams.GenePools[0].add(frams.GenMan.mutate(g.geno))
+
+for g in frams.GenePools[0]:
+	print(str(g.index) + '. "' + str(g.name) + '"  ' + str(g.genotype) + '  data=' + str(g.data))
Index: framspy/frams.py
===================================================================
--- framspy/frams.py	(revision 1078)
+++ framspy/frams.py	(revision 1078)
@@ -0,0 +1,307 @@
+"""Framsticks as a Python module.
+
+Static FramScript objects are available inside the module under their well known names
+(frams.Simulator, frams.GenePools, etc.)
+
+These objects and all values passed to and from Framsticks are instances of frams.ExtValue.
+Python values are automatically converted to Framstics data types.
+Use frams.ExtValue._makeInt()/_makeDouble()/_makeString()/_makeNull() for explicit conversions.
+Simple values returned from Framsticks can be converted to their natural Python
+counterparts using _value() (or forced to a specific type with  _int()/_double()/_string()).
+
+All non-Framsticks Python attributes start with '_' to avoid conflicts with Framsticks attributes.
+Framsticks names that are Python reserved words are prefixed with 'x' (currently just Simulator.ximport).
+
+"""
+
+import ctypes, re, sys, os
+
+c_api = None  # will be initialized in init(). Global because ExtValue uses it.
+
+
+class ExtValue(object):
+	_reInsideParens = re.compile('\((.*)\)')
+	_reservedWords = ['import']  # this list is scanned during every attribute access, only add what is really clashing with framsticks properties
+	_reservedXWords = ['x' + word for word in _reservedWords]
+	_encoding = 'utf-8'
+
+
+	def __init__(self, arg=None, dontinit=False):
+		if dontinit:
+			return
+		if isinstance(arg, int):
+			self._initFromInt(arg)
+		elif isinstance(arg, str):
+			self._initFromString(arg)
+		elif isinstance(arg, float):
+			self._initFromDouble(arg)
+		elif arg == None:
+			self._initFromNull()
+		else:
+			raise ArgumentError("Can't make ExtValue from '" + str(arg) + "'")
+
+
+	def __del__(self):
+		c_api.extFree(self.__ptr)
+
+
+	def _initFromNull(self):
+		self.__ptr = c_api.extFromNull()
+
+
+	def _initFromInt(self, v):
+		self.__ptr = c_api.extFromInt(v)
+
+
+	def _initFromDouble(self, v):
+		self.__ptr = c_api.extFromDouble(v)
+
+
+	def _initFromString(self, v):
+		self.__ptr = c_api.extFromString(ExtValue._cstringFromPython(v))
+
+
+	@classmethod
+	def _makeNull(cls, v):
+		e = ExtValue(None, True)
+		e._initFromNull()
+		return e
+
+
+	@classmethod
+	def _makeInt(cls, v):
+		e = ExtValue(None, True)
+		e._initFromInt(v)
+		return e
+
+
+	@classmethod
+	def _makeDouble(cls, v):
+		e = ExtValue(None, True)
+		e._initFromDouble(v)
+		return e
+
+
+	@classmethod
+	def _makeString(cls, v):
+		e = ExtValue(None, True)
+		e._initFromString(v)
+		return e
+
+
+	@classmethod
+	def _rootObject(cls):
+		e = ExtValue(None, True)
+		e.__ptr = c_api.rootObject()
+		return e
+
+
+	@classmethod
+	def _stringFromC(cls, cptr):
+		return cptr.decode(ExtValue._encoding)
+
+
+	@classmethod
+	def _cstringFromPython(cls, s):
+		return ctypes.c_char_p(s.encode(ExtValue._encoding))
+
+
+	def _type(self):
+		return c_api.extType(self.__ptr)
+
+
+	def _class(self):
+		cls = c_api.extClass(self.__ptr)
+		if cls == None:
+			return None
+		else:
+			return ExtValue._stringFromC(cls)
+
+
+	def _value(self):
+		t = self._type()
+		if t == 0:
+			return None
+		elif t == 1:
+			return self._int()
+		elif t == 2:
+			return self._double()
+		elif t == 3:
+			return self._string()
+		else:
+			return self
+
+
+	def _int(self):
+		return c_api.extIntValue(self.__ptr)
+
+
+	def _double(self):
+		return c_api.extDoubleValue(self.__ptr)
+
+
+	def _string(self):
+		return ExtValue._stringFromC(c_api.extStringValue(self.__ptr))
+
+
+	def __str__(self):
+		return self._string()
+
+
+	def __dir__(self):
+		ids = dir(type(self))
+		if self._type() == 4:
+			for i in range(c_api.extPropCount(self.__ptr)):
+				name = ExtValue._stringFromC(c_api.extPropId(self.__ptr, i))
+				if name in ExtValue._reservedWords:
+					name = 'x' + name
+				ids.append(name)
+		return ids
+
+
+	def __getattr__(self, key):
+		if key[0] == '_':
+			return self.__dict__[key]
+		if key in ExtValue._reservedXWords:
+			key = key[1:]
+		prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
+		if prop_i < 0:
+			raise AttributeError('no ' + str(key) + ' in ' + str(self))
+		t = ExtValue._stringFromC(c_api.extPropType(self.__ptr, prop_i))
+		if t[0] == 'p':
+			arg_types = ExtValue._reInsideParens.search(t)
+			if arg_types:
+				arg_types = arg_types.group(1).split(',')  # anyone wants to add argument type validation using param type declarations?
+
+
+			def fun(*args):
+				ext_args = []
+				ext_pointers = []
+				for a in args:
+					if isinstance(a, ExtValue):
+						ext = a
+					else:
+						ext = ExtValue(a)
+					ext_args.append(ext)
+					ext_pointers.append(ext.__ptr)
+				ret = ExtValue(None, True)
+				args_array = (ctypes.c_void_p * len(args))(*ext_pointers)
+				ret.__ptr = c_api.extPropCall(self.__ptr, prop_i, len(args), args_array)
+				return ret
+
+
+			return fun
+		else:
+			ret = ExtValue(None, True)
+			ret.__ptr = c_api.extPropGet(self.__ptr, prop_i)
+			return ret
+
+
+	def __setattr__(self, key, value):
+		if key[0] == '_':
+			self.__dict__[key] = value
+		else:
+			if key in ExtValue._reservedXWords:
+				key = key[1:]
+			prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
+			if prop_i < 0:
+				raise AttributeError("No '" + str(key) + "' in '" + str(self) + "'")
+			if not isinstance(value, ExtValue):
+				value = ExtValue(value)
+			c_api.extPropSet(self.__ptr, prop_i, value.__ptr)
+
+
+	def __getitem__(self, key):
+		return self.get(key)
+
+
+	def __setitem__(self, key, value):
+		return self.set(key, value)
+
+
+	def __len__(self):
+		try:
+			return self.size._int()
+		except:
+			return 0
+
+
+	def __iter__(self):
+		class It(object):
+			def __init__(self, container, frams_it):
+				self.container = container
+				self.frams_it = frams_it
+
+
+			def __iter__(self):
+				return self
+
+
+			def __next__(self):
+				if self.frams_it.next._int() != 0:
+					return self.frams_it.value
+				else:
+					raise StopIteration()
+
+		return It(self, self.iterator)
+
+
+def init(dllpath, *args):
+	original_dir = os.getcwd()
+	os.chdir(dllpath)  # because under Windows, frams.dll requires other dll's which reside in the same directory, so we must change current dir for them to be found
+	global c_api
+	c_api = ctypes.CDLL('frams_c_api.so' if os.name == 'posix' else 'frams.dll')
+	os.chdir(original_dir)
+
+	c_api.init.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
+	c_api.init.restype = None
+	c_api.extFree.argtypes = [ctypes.c_void_p]
+	c_api.extFree.restype = None
+	c_api.extType.argtypes = [ctypes.c_void_p]
+	c_api.extType.restype = ctypes.c_int
+	c_api.extFromNull.argtypes = []
+	c_api.extFromNull.restype = ctypes.c_void_p
+	c_api.extFromInt.argtypes = [ctypes.c_int]
+	c_api.extFromInt.restype = ctypes.c_void_p
+	c_api.extFromDouble.argtypes = [ctypes.c_double]
+	c_api.extFromDouble.restype = ctypes.c_void_p
+	c_api.extFromString.argtypes = [ctypes.c_char_p]
+	c_api.extFromString.restype = ctypes.c_void_p
+	c_api.extIntValue.argtypes = [ctypes.c_void_p]
+	c_api.extIntValue.restype = ctypes.c_int
+	c_api.extDoubleValue.argtypes = [ctypes.c_void_p]
+	c_api.extDoubleValue.restype = ctypes.c_double
+	c_api.extStringValue.argtypes = [ctypes.c_void_p]
+	c_api.extStringValue.restype = ctypes.c_char_p
+	c_api.extClass.argtypes = [ctypes.c_void_p]
+	c_api.extClass.restype = ctypes.c_char_p
+	c_api.extPropCount.argtypes = [ctypes.c_void_p]
+	c_api.extPropCount.restype = ctypes.c_int
+	c_api.extPropId.argtypes = [ctypes.c_void_p, ctypes.c_int]
+	c_api.extPropId.restype = ctypes.c_char_p
+	c_api.extPropType.argtypes = [ctypes.c_void_p, ctypes.c_int]
+	c_api.extPropType.restype = ctypes.c_char_p
+	c_api.extPropFind.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
+	c_api.extPropFind.restype = ctypes.c_int
+	c_api.extPropGet.argtypes = [ctypes.c_void_p, ctypes.c_int]
+	c_api.extPropGet.restype = ctypes.c_void_p
+	c_api.extPropSet.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
+	c_api.extPropSet.restype = ctypes.c_int
+	c_api.extPropCall.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]
+	c_api.extPropCall.restype = ctypes.c_void_p
+	c_api.rootObject.argtypes = []
+	c_api.rootObject.restype = ctypes.c_void_p
+
+	c_args = (ctypes.c_char_p * (len(args) + 1))(*([b''] + list(a.encode(ExtValue._encoding) for a in args)))
+	c_api.init(len(args) + 1, c_args)
+	Root = ExtValue._rootObject()
+	for n in dir(Root):
+		if n[0].isalpha():
+			attr = getattr(Root, n)
+			if isinstance(attr, ExtValue):
+				attr = attr._value()
+			setattr(sys.modules[__name__], n, attr)
+	print('Using Framsticks version: ' + str(Simulator.version_string))
+	print('Home (writable) dir     : ' + home_dir)
+	print('Resources dir           : ' + res_dir)
+	print()
