Index: /exampleparties/boulware/requirements.txt
===================================================================
--- /exampleparties/boulware/requirements.txt	(revision 83)
+++ /exampleparties/boulware/requirements.txt	(revision 84)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/80/exampleparties/timedependentparty/dist/timedependentparty-1.1.3.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/83/exampleparties/timedependentparty/dist/timedependentparty-1.1.4.tar.gz
 mypy
 
Index: /exampleparties/boulware/setup.py
===================================================================
--- /exampleparties/boulware/setup.py	(revision 83)
+++ /exampleparties/boulware/setup.py	(revision 84)
@@ -3,11 +3,11 @@
 setup(
     name='boulware',
-    version='1.1.3',    
+    version='1.1.4',
     description='A python3 party that places bids with monotonically decreasing utility',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
-    packages = find_packages(exclude=["test", "test.*","test.*.*"]),
-    package_data = { 'boulware': ['py.typed']    },
-    install_requires=[ "timedependentparty@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/80/exampleparties/timedependentparty/dist/timedependentparty-1.1.3.tar.gz"],
+    packages=find_packages(exclude=["test", "test.*", "test.*.*"]),
+    package_data={ 'boulware': ['py.typed']    },
+    install_requires=[ "timedependentparty@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/83/exampleparties/timedependentparty/dist/timedependentparty-1.1.4.tar.gz"],
     py_modules=['party']
 )
Index: /exampleparties/conceder/requirements.txt
===================================================================
--- /exampleparties/conceder/requirements.txt	(revision 83)
+++ /exampleparties/conceder/requirements.txt	(revision 84)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/80/exampleparties/timedependentparty/dist/timedependentparty-1.1.3.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/83/exampleparties/timedependentparty/dist/timedependentparty-1.1.4.tar.gz
 mypy
 
Index: /exampleparties/conceder/setup.py
===================================================================
--- /exampleparties/conceder/setup.py	(revision 83)
+++ /exampleparties/conceder/setup.py	(revision 84)
@@ -3,11 +3,11 @@
 setup(
     name='conceder',
-    version='1.1.3',    
+    version='1.1.4',
     description='A python3 party that places bids with monotonically decreasing utility',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
-    packages = find_packages(exclude=["test", "test.*","test.*.*"]),
-    package_data = { 'conceder': ['py.typed']    },
-    install_requires=[ "timedependentparty@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/80/exampleparties/timedependentparty/dist/timedependentparty-1.1.3.tar.gz"],
+    packages=find_packages(exclude=["test", "test.*", "test.*.*"]),
+    package_data={ 'conceder': ['py.typed']    },
+    install_requires=[ "timedependentparty@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/83/exampleparties/timedependentparty/dist/timedependentparty-1.1.4.tar.gz"],
     py_modules=['party']
 )
Index: /exampleparties/hardliner/requirements.txt
===================================================================
--- /exampleparties/hardliner/requirements.txt	(revision 83)
+++ /exampleparties/hardliner/requirements.txt	(revision 84)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/80/exampleparties/timedependentparty/dist/timedependentparty-1.1.3.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/83/exampleparties/timedependentparty/dist/timedependentparty-1.1.4.tar.gz
 mypy
 
Index: /exampleparties/hardliner/setup.py
===================================================================
--- /exampleparties/hardliner/setup.py	(revision 83)
+++ /exampleparties/hardliner/setup.py	(revision 84)
@@ -3,11 +3,11 @@
 setup(
     name='hardliner',
-    version='1.1.3',    
+    version='1.1.4',
     description='A python3 party that places bids with monotonically decreasing utility',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
-    packages = find_packages(exclude=["test", "test.*","test.*.*"]),
-    package_data = { 'hardliner': ['py.typed']    },
-    install_requires=[ "timedependentparty@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/80/exampleparties/timedependentparty/dist/timedependentparty-1.1.3.tar.gz"],
+    packages=find_packages(exclude=["test", "test.*", "test.*.*"]),
+    package_data={ 'hardliner': ['py.typed']    },
+    install_requires=[ "timedependentparty@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/83/exampleparties/timedependentparty/dist/timedependentparty-1.1.4.tar.gz"],
     py_modules=['party']
 )
Index: /exampleparties/linear/requirements.txt
===================================================================
--- /exampleparties/linear/requirements.txt	(revision 83)
+++ /exampleparties/linear/requirements.txt	(revision 84)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/80/exampleparties/timedependentparty/dist/timedependentparty-1.1.3.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/83/exampleparties/timedependentparty/dist/timedependentparty-1.1.4.tar.gz
 mypy
 
Index: /exampleparties/linear/setup.py
===================================================================
--- /exampleparties/linear/setup.py	(revision 83)
+++ /exampleparties/linear/setup.py	(revision 84)
@@ -3,11 +3,11 @@
 setup(
     name='linear',
-    version='1.1.3',    
+    version='1.1.4',
     description='A python3 party that places bids with monotonically decreasing utility',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
-    packages = find_packages(exclude=["test", "test.*","test.*.*"]),
-    package_data = { 'linear': ['py.typed']    },
-    install_requires=[ "timedependentparty@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/80/exampleparties/timedependentparty/dist/timedependentparty-1.1.3.tar.gz"],
+    packages=find_packages(exclude=["test", "test.*", "test.*.*"]),
+    package_data={ 'linear': ['py.typed']    },
+    install_requires=[ "timedependentparty@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/83/exampleparties/timedependentparty/dist/timedependentparty-1.1.4.tar.gz"],
     py_modules=['party']
 )
Index: /exampleparties/randomparty/requirements.txt
===================================================================
--- /exampleparties/randomparty/requirements.txt	(revision 83)
+++ /exampleparties/randomparty/requirements.txt	(revision 84)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/79/geniuswebcore/dist/geniusweb-1.1.3.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/82/geniuswebcore/dist/geniusweb-1.1.4.tar.gz
 mypy
 
Index: /exampleparties/randomparty/setup.py
===================================================================
--- /exampleparties/randomparty/setup.py	(revision 83)
+++ /exampleparties/randomparty/setup.py	(revision 84)
@@ -3,10 +3,10 @@
 setup(
     name='randomparty',
-    version='1.1.3',    
+    version='1.1.4',
     description='A python3 party that places random bids with sufficient utility',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
     packages=['randomparty'],
-    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/79/geniuswebcore/dist/geniusweb-1.1.3.tar.gz"],
+    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/82/geniuswebcore/dist/geniusweb-1.1.4.tar.gz"],
     py_modules=['party']
 )
Index: /exampleparties/stupidparty/requirements.txt
===================================================================
--- /exampleparties/stupidparty/requirements.txt	(revision 83)
+++ /exampleparties/stupidparty/requirements.txt	(revision 84)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/79/geniuswebcore/dist/geniusweb-1.1.3.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/82/geniuswebcore/dist/geniusweb-1.1.4.tar.gz
 mypy
 
Index: /exampleparties/stupidparty/setup.py
===================================================================
--- /exampleparties/stupidparty/setup.py	(revision 83)
+++ /exampleparties/stupidparty/setup.py	(revision 84)
@@ -3,10 +3,10 @@
 setup(
     name='stupidparty',
-    version='1.1.3',    
+    version='1.1.4',
     description='A example python3 party module',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
     packages=['stupidparty'],
-    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/79/geniuswebcore/dist/geniusweb-1.1.3.tar.gz"],
+    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/82/geniuswebcore/dist/geniusweb-1.1.4.tar.gz"],
     py_modules=['party'],
 
Index: /exampleparties/timedependentparty/requirements.txt
===================================================================
--- /exampleparties/timedependentparty/requirements.txt	(revision 83)
+++ /exampleparties/timedependentparty/requirements.txt	(revision 84)
@@ -1,3 +1,3 @@
-https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/79/geniuswebcore/dist/geniusweb-1.1.3.tar.gz
+https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/82/geniuswebcore/dist/geniusweb-1.1.4.tar.gz
 mypy
 
Index: /exampleparties/timedependentparty/setup.py
===================================================================
--- /exampleparties/timedependentparty/setup.py	(revision 83)
+++ /exampleparties/timedependentparty/setup.py	(revision 84)
@@ -3,11 +3,11 @@
 setup(
     name='timedependentparty',
-    version='1.1.3',    
+    version='1.1.4',
     description='A python3 party that places bids with monotonically decreasing utility',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
     author='W.Pasman',
-    packages = find_packages(exclude=["test", "test.*","test.*.*"]),
-    package_data = { 'timedependentparty': ['py.typed']    },
-    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/79/geniuswebcore/dist/geniusweb-1.1.3.tar.gz"],
+    packages=find_packages(exclude=["test", "test.*", "test.*.*"]),
+    package_data={ 'timedependentparty': ['py.typed']    },
+    install_requires=[ "geniusweb@https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWebPython/export/82/geniuswebcore/dist/geniusweb-1.1.4.tar.gz"],
     py_modules=['party']
 )
Index: /exampleparties/timedependentparty/timedependentparty/TimeDependentParty.py
===================================================================
--- /exampleparties/timedependentparty/timedependentparty/TimeDependentParty.py	(revision 83)
+++ /exampleparties/timedependentparty/timedependentparty/TimeDependentParty.py	(revision 84)
@@ -41,9 +41,5 @@
 class TimeDependentParty (DefaultParty):
     """
-    General time dependent party. This is a simplistic implementation that does
-    brute-force search through the bidspace and can handle bidspace sizes up to
-    2^31 (approx 1 billion bids). It may take excessive time and run out of time
-    on bidspaces &gt; 10000 bids. In special cases it may even run out of memory,
-    
+    General time dependent party.
     <p>
     Supports parameters as follows
@@ -82,13 +78,13 @@
     def __init__(self, reporter:Reporter=None):
         super().__init__(reporter)
-        self._profileint:ProfileInterface = None #ttype:ignore
-        self._utilspace:LinearAdditive  = None # last received space
-        self._me:PartyId =None #ttype:ignore
-        self._progress:Progress =None #ttype:ignore
-        self._lastReceivedBid:Bid = None
-        self._extendedspace:ExtendedUtilSpace =None
+        self._profileint:ProfileInterface = None #type:ignore
+        self._utilspace:LinearAdditive  = None #type:ignore
+        self._me:PartyId =None #type:ignore
+        self._progress:Progress =None #type:ignore
+        self._lastReceivedBid:Bid = None #type:ignore 
+        self._extendedspace:ExtendedUtilSpace =None #type:ignore
         self._e:float = 1.2
-        self._lastvotes:Votes =None
-        self._settings:Settings =None
+        self._lastvotes:Votes =None #type:ignore
+        self._settings:Settings =None #type:ignore
         self.getReporter().log(logging.INFO,"party is initialized")
 
Index: /geniuswebcore/geniusweb/bidspace/pareto/GenericPareto.py
===================================================================
--- /geniuswebcore/geniusweb/bidspace/pareto/GenericPareto.py	(revision 84)
+++ /geniuswebcore/geniusweb/bidspace/pareto/GenericPareto.py	(revision 84)
@@ -0,0 +1,88 @@
+import threading
+from typing import List, Set, Optional
+
+from geniusweb.bidspace.AllBidsList import AllBidsList
+from geniusweb.bidspace.pareto.ParetoFrontier import ParetoFrontier
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.profile.PartialOrdering import PartialOrdering
+from geniusweb.profile.Profile import Profile
+from geniusweb.utils import val
+
+
+class GenericPareto(ParetoFrontier):
+	'''
+	A set of all pareto points associated with a set of profiles. Generally
+	applicable but slow.
+	'''
+
+	def __init__(self, profiles:List[PartialOrdering]):
+		'''
+		Constructs pareto from a general {@link PartialOrdering}. This is a
+		brute-force algorithm that will inspect (but not store) ALL bids in the
+		space, and therefore may be inapproproate depending on the size of your
+		domain. Complexity O( |bidspace|^2 |profiles| )
+		
+		@param profiles a set of at least 2 {@link PartialOrdering}s. All must be
+		                defined on the same domain.
+		'''
+		if len(profiles) < 2:
+			raise ValueError("at least 2 profiles are needed")
+		
+		if None in profiles:
+			raise ValueError("Profiles must not be None")
+		domain = profiles[0].getDomain()
+		for profile in profiles:
+			if not domain == profile.getDomain():
+				raise ValueError(
+						"All profiles must be on same domain ("
+								+domain.getName() + ") but found " + str(profile));
+		self._profiles = list(profiles)
+		self._paretobids:Optional[Set[Bid]] = None
+		self._lock = threading.Lock()
+
+	# Override
+	def getProfiles(self) -> List[Profile]:
+		return list(self._profiles)
+
+	# Override
+	def getPoints(self) -> Set[Bid]:
+		with self._lock:
+			if self._paretobids == None:
+				self._computePareto()
+			return set(val(self._paretobids))
+
+	# Override
+	def __repr__(self):
+		return "Pareto " + str(self.getPoints())
+
+	def _computePareto(self):
+		'''
+		assigns {@link #paretobids}.
+		'''
+		self._paretobids:Set[Bid] = set()
+
+		for newbid in AllBidsList(self._profiles[0].getDomain()):
+			'''
+			invariant: paretobids contains bids not dominated by other bids
+			in paretobids. That means we need (1) check if new bid is
+			dominated (2) if existing bids are dominated if we add a new bid
+			 '''
+			newBidIsDominated = any(self._isDominatedBy(newbid, paretobid) 
+								for paretobid in self._paretobids)
+
+			# if new bid is not dominated, we add it and we re-check existing
+			# if they are now dominated
+			if not newBidIsDominated:
+				self._paretobids = {paretobid for paretobid in self._paretobids
+					if not self._isDominatedBy(paretobid, newbid)}
+				self._paretobids.add(newbid)
+
+	def _isDominatedBy(self, bid1:Bid , dominantbid: Bid) -> bool:
+		'''
+		@param bid1        the bid to check
+		@param dominantbid the bid that is supposed to dominate bid1
+		@return true iff bid1 is dominated by dominant bid. "Dominated by" means
+		        that the bid is preferred or equal in all profiles.
+		'''
+		return all(profile.isPreferredOrEqual(dominantbid, bid1)
+				for profile in self._profiles)
Index: /geniuswebcore/geniusweb/bidspace/pareto/ParetoFrontier.py
===================================================================
--- /geniuswebcore/geniusweb/bidspace/pareto/ParetoFrontier.py	(revision 84)
+++ /geniuswebcore/geniusweb/bidspace/pareto/ParetoFrontier.py	(revision 84)
@@ -0,0 +1,23 @@
+from typing import List, Set
+from geniusweb.profile.Profile import Profile
+from abc import ABC, abstractmethod
+from geniusweb.issuevalue.Bid import Bid
+
+class ParetoFrontier (ABC):
+	'''
+	The pareto frontier is the set of {@link Bid}s for which there is no other
+	bid that is similar or better for the involved profiles.
+	'''
+
+	@abstractmethod
+	def getProfiles(self) -> List[Profile] :
+		'''
+		@return the profiles that form the basis of this pareto frontier.
+		'''
+
+	@abstractmethod
+	def getPoints(self) -> Set[Bid] :
+		'''
+		@return the set of all {@link Bid}s that are on the pareto frontier.
+		'''
+
Index: /geniuswebcore/geniusweb/bidspace/pareto/ParetoLinearAdditive.py
===================================================================
--- /geniuswebcore/geniusweb/bidspace/pareto/ParetoLinearAdditive.py	(revision 84)
+++ /geniuswebcore/geniusweb/bidspace/pareto/ParetoLinearAdditive.py	(revision 84)
@@ -0,0 +1,51 @@
+from typing import List, Set, Optional
+
+from geniusweb import profile
+from geniusweb.bidspace.pareto.ParetoFrontier import ParetoFrontier
+from geniusweb.bidspace.pareto.PartialPareto import PartialPareto
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.profile.Profile import Profile
+from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
+
+
+class ParetoLinearAdditive(ParetoFrontier):
+	'''
+	pareto frontier implementation for {@link LinearAdditive}. This is a highly
+	optimized pareto method for {@link LinearAdditive} spaces
+	'''
+	
+	def __init__(self, utilSpaces:List[LinearAdditive]):
+		'''
+		@param utilSpaces. Must contain at least two {@link LinearAdditive}s and
+		                   all must be defined on the same donain.
+		'''
+		if utilSpaces == None or len(utilSpaces) < 2:
+			raise ValueError("utilSpaces must contain at least 2 spaces")
+
+		domain = utilSpaces[0].getDomain()
+		for space in utilSpaces:
+			if not space.getDomain() == domain:
+				raise ValueError(
+					"Expected all spaces using domain " + domain.getName()
+								+" but found " + space.getDomain().getName());
+		self._utilSpaces = utilSpaces
+		self._points:Optional[Set[Bid] ] = None
+
+	# Override
+	def getProfiles(self) -> List[Profile]:
+		return list(self._utilSpaces)
+
+	# Override
+	def getPoints(self) -> Set[Bid]:
+		if self._points == None:
+			self._points = self._computePareto()
+		return self._points  # type:ignore
+
+	# Override
+	def toString(self) -> str:
+		return "Pareto " + str(self.getPoints())
+
+	def _computePareto(self) -> Set[Bid]:
+		issues:List[str] = list(self._utilSpaces[0].getDomain().getIssues())
+		partial = PartialPareto.create(self._utilSpaces, issues)
+		return set([ point.getBid() for point in partial.getPoints() ])
Index: /geniuswebcore/geniusweb/bidspace/pareto/ParetoPoint.py
===================================================================
--- /geniuswebcore/geniusweb/bidspace/pareto/ParetoPoint.py	(revision 84)
+++ /geniuswebcore/geniusweb/bidspace/pareto/ParetoPoint.py	(revision 84)
@@ -0,0 +1,65 @@
+from decimal import Decimal
+from typing import List, Dict
+
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.issuevalue.Value import Value
+from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
+
+
+class ParetoPoint:
+	'''
+	A Paretopoint is a Bid together with an N-dimensional utility vector. This is
+	also a caching mechanism to avoid repeated utility computation of good bids.
+	This is a internal utility class to streamline pareto computations.
+	'''
+
+	def __init__(self, utils:List[Decimal] , bid:Bid):
+		'''
+		@param bid    a (possibly partial) {@link Bid}
+		@param utils the utilities of the bid in all spaces, in order
+		'''
+		self._bid = bid;
+		self._utilities = list(utils)
+
+	@staticmethod
+	def create(bid:Bid , spaces:List[LinearAdditive]) -> "ParetoPoint":
+		'''
+		@param bid    a (possibly partial) {@link Bid}
+		@param spaces the {@link LinearAdditive}s to consider
+		'''
+		utilities:List[Decimal] = []
+		for space in spaces:
+			utilities.append(space.getUtility(bid))
+		return ParetoPoint(utilities, bid)
+
+	def merge(self, otherpoint:"ParetoPoint") -> "ParetoPoint":
+		'''
+		Merges the issues from both bids and adds the utilities. This only works
+		correctly if the issues in other point are completely disjoint from our
+		bid issues.
+		
+		@param otherpoint with the utils summed and the issue values merged
+		'''
+		summedutils:List[Decimal] = []
+		for n in range(len(self._utilities)):
+			summedutils.append(self._utilities[n] + otherpoint._utilities[n])
+
+		return ParetoPoint(summedutils, self._bid.merge(otherpoint.getBid()))
+
+	def isDominatedBy(self, other:"ParetoPoint") -> bool:
+		'''
+		@param other
+		@return true if this ParetoPoint is dominated by the other. That means
+		        other has better or equal utilities in ALL dimensions.
+		'''
+		otherutils:List[Decimal] = other.getUtilities()
+		for i  in range(len(self._utilities)):
+			if otherutils[i] < self._utilities[i]:
+				return False
+		return True
+
+	def getUtilities(self) -> List[Decimal]:
+		return self._utilities
+
+	def getBid(self) -> Bid:
+		return self._bid
Index: /geniuswebcore/geniusweb/bidspace/pareto/PartialPareto.py
===================================================================
--- /geniuswebcore/geniusweb/bidspace/pareto/PartialPareto.py	(revision 84)
+++ /geniuswebcore/geniusweb/bidspace/pareto/PartialPareto.py	(revision 84)
@@ -0,0 +1,89 @@
+from typing import List, Dict
+
+from geniusweb.bidspace.pareto.ParetoPoint import ParetoPoint
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.issuevalue.Value import Value
+from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
+
+
+class PartialPareto:
+	'''
+	 PartialPareto contains a pareto surface of partial bids. Internal-only
+	 utility class for computing LinearAdditive pareto surface. Immutable.
+	'''
+
+	def __init__(self, points:List[ParetoPoint]):
+		'''
+		@param points the points to be contained in this PartialPareto
+'''
+		self._points = points
+
+	@staticmethod
+	def create(utilSpaces:List[LinearAdditive] ,
+			issues: List[str]) -> "PartialPareto":
+		'''
+		Constructor. Avoided the standard 'new' constructor as this is
+		implemented recursively (O(log(#issues))).
+		
+		@param utilSpaces the {@link LinearAdditive}s
+		@param issues     the issues (subset of all issues in the space) to be
+		                  used for this pareto
+		@return the pareto surface (non-dominated bids) when considering only the
+		        given issues.
+		'''
+
+		if len(issues) == 1:
+			issue = issues[0]
+			map:Dict[str, Value] = {}
+			list:List[ParetoPoint] = []
+			for value in utilSpaces[0].getDomain().getValues(issue):
+				map[issue] = value
+				list = PartialPareto._add(list, ParetoPoint.create(Bid(map), utilSpaces))
+			return PartialPareto(list)
+		else:
+			halfway = int(len(issues) / 2)
+			return PartialPareto.create(utilSpaces, issues[0: halfway])._merge(
+					PartialPareto.create(utilSpaces, issues[halfway:]))
+
+	def getPoints(self) -> List[ParetoPoint]:
+		'''
+		@return the ParetoPoints on the (possibly partial) Pareto surface
+		'''
+		return self._points
+
+	def _merge(self, other:"PartialPareto") -> "PartialPareto":
+		'''
+		This combines two partial paretos. Here is the heart of the algorithm.
+		
+		@param other another {@link PartialPareto} to be merged with this. The
+		             other pareto must be composed with non-overlapping range.
+		@return merge of two partial paretos. Checks all partial1*partial2
+		        combinations and only non-dominated points are kept.
+		'''
+		merge:List[ParetoPoint] = []
+		for point in  self._points:
+			for otherpoint in other._points:
+				merge = PartialPareto._add(merge, point.merge(otherpoint))
+		return PartialPareto(merge)
+
+	@staticmethod
+	def _add(list:List[ParetoPoint], candidate:ParetoPoint) -> List[ParetoPoint]:
+		'''
+		Add a new ParetoPoint to a list
+		
+		@param list      the existing list
+		@param candidate a new {@link ParetoPoint} candidate
+		@return a new list that either ignored the candidate, or added it and
+		        removed the old points that are dominated by the candidate
+		'''
+		for existing in list:
+			if candidate.isDominatedBy(existing):
+				return list
+		# if we get here, candidate is not dominated so we add it
+		# and remove existing ones now dominated
+		newlist:List[ParetoPoint] = []
+		newlist.append(candidate)
+		for existing in list:
+			if not existing.isDominatedBy(candidate):
+				newlist.append(existing)
+		return newlist
Index: /geniuswebcore/geniusweb/issuevalue/Bid.py
===================================================================
--- /geniuswebcore/geniusweb/issuevalue/Bid.py	(revision 83)
+++ /geniuswebcore/geniusweb/issuevalue/Bid.py	(revision 84)
@@ -1,3 +1,3 @@
-from typing import Any, Dict, Union, Set
+from typing import Any, Dict, Union, Set, Optional
 from geniusweb.issuevalue.Value import Value
 from geniusweb.utils import toStr
@@ -31,5 +31,5 @@
     
     
-    def getValue(self, issue:str) -> Union[Value,NoneType] :
+    def getValue(self, issue:str) -> Optional[Value] :
         '''
         @param issue name of the issue
Index: /geniuswebcore/geniusweb/opponentmodel/FrequencyOpponentModel.py
===================================================================
--- /geniuswebcore/geniusweb/opponentmodel/FrequencyOpponentModel.py	(revision 84)
+++ /geniuswebcore/geniusweb/opponentmodel/FrequencyOpponentModel.py	(revision 84)
@@ -0,0 +1,170 @@
+from geniusweb.profile.utilityspace.UtilitySpace import UtilitySpace
+from geniusweb.opponentmodel.OpponentModel import OpponentModel 
+from decimal import Decimal
+from geniusweb.issuevalue.Domain import Domain
+from geniusweb.issuevalue.Bid import Bid
+from typing import Dict, Optional
+from geniusweb.issuevalue.Value import Value
+from geniusweb.actions.Action import Action
+from geniusweb.progress.Progress import Progress
+from geniusweb.actions.Offer import Offer
+from geniusweb.references.Parameters import Parameters
+from geniusweb.utils import val, HASH, toStr
+
+class FrequencyOpponentModel(UtilitySpace, OpponentModel):
+	'''
+	implements an {@link OpponentModel} by counting frequencies of bids placed by
+	the opponent.
+	<p>
+	NOTE: {@link NumberValue}s are also treated as 'discrete', so the frequency
+	of one value does not influence the influence the frequency of nearby values
+	(as you might expect as {@link NumberValueSetUtilities} is only affected by
+	the endpoints).
+	<p>
+	immutable.
+	'''
+
+	_DECIMALS = 4 # accuracy of our computations.
+
+	def __init__(self, domain:Optional[Domain],
+			freqs:Dict[str, Dict[Value, int]] ,  total:int,
+			resBid:Optional[Bid] ) :
+		'''
+		internal constructor. DO NOT USE, see create. Assumes the freqs keyset is 
+		equal to the available issues.
+		
+		@param domain the domain. Should not be None
+		@param freqs  the observed frequencies for all issue values. This map is
+		              assumed to be a fresh private-access only copy.
+		@param total  the total number of bids contained in the freqs map. This
+		              must be equal to the sum of the Integer values in the
+		              {@link #bidFrequencies} for each issue (this is not
+		              checked).
+		@param resBid the reservation bid. Can be null
+		'''
+		self._domain = domain
+		self._bidFrequencies = freqs
+		self._totalBids = total
+		self._resBid = resBid
+
+	@staticmethod
+	def create() -> "FrequencyOpponentModel":
+		return FrequencyOpponentModel(None, {}, 0 ,None )
+
+	#Override
+	def  With(self, newDomain:Domain,  newResBid:Optional[Bid]) -> "FrequencyOpponentModel":
+		if newDomain == None:
+			raise ValueError("domain is not initialized")
+		# FIXME merge already available frequencies?
+		return FrequencyOpponentModel(newDomain,
+				{ iss: {} for iss in newDomain.getIssues() },					
+				0, newResBid)
+
+	#Override
+	def getUtility(self,bid:Bid ) -> Decimal:
+		if self._domain == None: 
+			raise ValueError("domain is not initialized")
+		if self._totalBids == 0:
+			return Decimal(1)
+		sum = Decimal(0)
+		# Assume all issues have equal weight.
+		for issue in val(self._domain).getIssues():
+			if issue in bid.getIssues():
+				sum = sum + self._getFraction(issue, val(bid.getValue(issue)))
+		return round(sum  / len(self._bidFrequencies), FrequencyOpponentModel._DECIMALS)
+
+	#Override
+	def getName(self)->str:
+		if self._domain == None:
+			raise ValueError("domain is not initialized")
+		return "FreqOppModel" + str(hash(self)) + "For" + str(self._domain)
+
+	#Override
+	def getDomain(self)->Domain :
+		return val(self._domain)
+
+	#Override
+	def WithAction(self,  action:Action,  progress:Progress) ->"FrequencyOpponentModel":
+		if self._domain == None:
+			raise ValueError("domain is not initialized")
+
+		if not isinstance(action,Offer):
+			return self
+
+		bid:Bid = action.getBid()
+		newFreqs:Dict[str, Dict[Value, int]] = self.cloneMap(self._bidFrequencies)
+		for issue in self._domain.getIssues(): #type:ignore
+			freqs:Dict[Value, int]  = newFreqs[issue]
+			value = bid.getValue(issue)
+			if value != None:
+				oldfreq=0
+				if value in freqs:
+					oldfreq = freqs[value]
+				freqs[value]=oldfreq + 1 #type:ignore
+
+		return FrequencyOpponentModel(self._domain, newFreqs,
+				self._totalBids+1, self._resBid)
+
+	def getCounts(self, issue:str) -> Dict[Value, int] :
+		'''
+		@param issue the issue to get frequency info for
+		@return a map containing a map of values and the number of times that
+		        value was used in previous bids. Values that are possible but not
+		        in the map have frequency 0.
+		'''
+		if self._domain == None:
+			raise ValueError("domain is not initialized")
+		if not  issue in self._bidFrequencies:
+			return {}
+		return dict(self._bidFrequencies.get(issue)) #type:ignore
+
+	#Override
+	def WithParameters(self,parameters:Parameters ) ->OpponentModel :
+		return self # ignore parameters
+
+	def _getFraction(self, issue:str, value:Value ) -> Decimal :
+		'''
+		@param issue the issue to check
+		@param value the value to check
+		@return the fraction of the total cases that bids contained given value
+		        for the issue.
+		'''
+		if self._totalBids == 0:
+			return Decimal(1)
+		if not (issue in self._bidFrequencies and value in self._bidFrequencies[issue]):
+			return Decimal(0)
+		freq:int = self._bidFrequencies[issue][value]
+		return round(Decimal(freq) / self._totalBids, FrequencyOpponentModel._DECIMALS) #type:ignore
+
+	@staticmethod
+	def cloneMap(freqs: Dict[str, Dict[Value, int]]) -> Dict[str, Dict[Value, int]] :
+		'''
+		@param freqs
+		@return deep copy of freqs map.
+		'''
+		map:Dict[str, Dict[Value, int]] = {}
+		for issue in freqs:
+			map[issue]=dict(freqs[issue])
+		return map
+
+	#Override
+	def getReservationBid(self) -> Optional[Bid] :
+		return self._resBid
+
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and \
+			 self._domain == other._domain and\
+			 self._bidFrequencies == other._bidFrequencies and\
+			 self._totalBids == other._totalBids and\
+			 self._resBid == other._resBid
+
+
+	def __hash__(self):
+		return HASH((self._domain, self._bidFrequencies, self._totalBids, self._resBid))
+	#Override
+
+	#Override
+	def __repr__(self)->str:
+		return "FrequencyOpponentModel[" + str(self._totalBids) + "," + \
+			toStr(self._bidFrequencies) + "]"
Index: /geniuswebcore/geniusweb/opponentmodel/OpponentModel.py
===================================================================
--- /geniuswebcore/geniusweb/opponentmodel/OpponentModel.py	(revision 84)
+++ /geniuswebcore/geniusweb/opponentmodel/OpponentModel.py	(revision 84)
@@ -0,0 +1,57 @@
+from abc import ABC, abstractmethod
+from geniusweb.issuevalue.Domain import Domain
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.references.Parameters import Parameters
+from geniusweb.actions.Action import Action
+from geniusweb.progress.Progress import Progress
+
+class OpponentModel(ABC):
+	'''
+	An opponentmodel estimates a {@link UtilitySpace} from received opponent
+	actions.
+	<h2>Requirement</h2> A OpponentModel must have a constructor that takes the
+	Domain as argument. unfortunately this can not be enforced in a java
+	interface
+	
+	<p>
+	<em>MUST</em> have an empty constructor as these are also used as part of the
+	BOA framework.
+	'''
+	
+	@abstractmethod
+	def With(self, domain:Domain , resBid:Bid) -> "OpponentModel":
+		'''
+		Initializes the model. This function must be called first after
+		constructing an instance. It can also be called again later, if there is
+		a change in the domain or resBid.
+		<p>
+		This late-initialization is to support boa models that have late
+		initialization.
+		
+		@param domain the domain to work with. Must be not null.
+		@param resBid the reservation bid, or null if no reservationbid is
+		              available.
+		@return OpponentModel that uses given domain and reservationbid.
+		'''
+
+	@abstractmethod
+	def WithParameters(self, parameters:Parameters ) -> "OpponentModel" :
+		'''
+		@param parameters Opponent-model specific {@link Parameters}
+		@return an updated OpponentMode, with parameters used. Each
+		        implementation of OpponentModel is free to use parameters as it
+		        likes. For instance to set learning speed.
+		'''
+
+	@abstractmethod
+	def WithAction(self, action:Action , progress:Progress ) -> "OpponentModel" :
+		'''
+		Update this with a new action that was done by the opponent that this
+		model is modeling. {@link #with(Domain, Bid)} must be called before
+		calling this.
+		
+		@param action   the new incoming action.
+		@param progress the current progress of the negotiation. Calls to this
+		                must be done with increasing progress.
+		@return the updated {@link OpponentModel}
+		'''
Index: /geniuswebcore/setup.py
===================================================================
--- /geniuswebcore/setup.py	(revision 83)
+++ /geniuswebcore/setup.py	(revision 84)
@@ -3,5 +3,5 @@
 setup(
     name='geniusweb',
-    version='1.1.3',    
+    version='1.1.4',    
     description='GeniusWeb glue code to connect python3 parties',
     url='https://tracinsy.ewi.tudelft.nl/pubtrac/GeniusWeb',
Index: /geniuswebcore/test/geniusweb/bidspace/pareto/GenericParetoTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/bidspace/pareto/GenericParetoTest.py	(revision 84)
+++ /geniuswebcore/test/geniusweb/bidspace/pareto/GenericParetoTest.py	(revision 84)
@@ -0,0 +1,149 @@
+from decimal import Decimal
+from typing import Dict, List
+import unittest
+from unittest.mock import Mock
+
+from tudelft.utilities.immutablelist.Range import Range
+
+from geniusweb.bidspace.pareto.GenericPareto import GenericPareto
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.issuevalue.DiscreteValue import DiscreteValue
+from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet
+from geniusweb.issuevalue.Domain import Domain
+from geniusweb.issuevalue.NumberValue import NumberValue
+from geniusweb.issuevalue.NumberValueSet import NumberValueSet
+from geniusweb.issuevalue.Value import Value
+from geniusweb.issuevalue.ValueSet import ValueSet
+from geniusweb.profile.PartialOrdering import PartialOrdering
+
+
+class GenericParetoTest(unittest.TestCase):
+	I1V2 = DiscreteValue("i1v2")
+	I1V1 = DiscreteValue("i1v1")
+	I2V1 = NumberValue(Decimal("2.00"))
+	I2V2 = NumberValue(Decimal("2.45"))
+	I2V3 = NumberValue(Decimal("2.90"))
+	DOMAINNAME = "testdomain"
+	ISSUE1 = "issue1"
+	ISSUE2 = "issue2"
+	issues:Dict[str, ValueSet] = {}
+
+	def setUp(self):
+		discretevalues1 = []
+		discretevalues1.append(self.I1V1)
+		discretevalues1.append(self.I1V2)
+		values1 = DiscreteValueSet(discretevalues1)
+		self.issues[self.ISSUE1] = values1
+
+		values2 = NumberValueSet(Range(Decimal(2), Decimal(3), Decimal("0.45")))
+		self.issues[self.ISSUE2] = values2
+
+		self.domain = Domain(self.DOMAINNAME, self.issues)
+
+		issuevalues:Dict[str, Value] = {}
+		issuevalues[self.ISSUE1] = self.I1V1
+		issuevalues[self.ISSUE2] = self.I2V1
+		self.bid1 = Bid(issuevalues)
+		issuevalues[self.ISSUE1] = self.I1V1
+		issuevalues[self.ISSUE2] = self.I2V2
+		self.bid2 = Bid(issuevalues)
+		issuevalues[self.ISSUE1] = self.I1V2
+		issuevalues[self.ISSUE2] = self.I2V1
+		self.bid3 = Bid(issuevalues)
+
+		self.profile1 = Mock(PartialOrdering)
+		self.profile2 = Mock(PartialOrdering)
+		self.profile3 = Mock(PartialOrdering)
+		self.profile1.getDomain = Mock(return_value=self.domain)
+		self.profile2.getDomain = Mock(return_value=self.domain)
+		self.profile3.getDomain = Mock(return_value=self.domain)
+
+		self.pareto = GenericPareto([self.profile1, self.profile2])
+
+	def testgenericParetoTest(self):
+
+		# fefault: there is no preference at all, all isPreferredOrEqual
+		# returns false and we can't remove pareto points.
+
+		profiles:List[PartialOrdering] = [self.profile1, self.profile2]
+		# In python we need to set also False values
+		self.profile1.isPreferredOrEqual = Mock(return_value=False)
+		self.profile2.isPreferredOrEqual = Mock(return_value=False)
+		pareto = GenericPareto(profiles)
+
+		points = pareto.getPoints()
+		self.assertEqual(6, len(points))
+
+	def testgenericParetoTest1(self):
+
+		# both prefer bid1 over any other bid.
+		self.profile1.isPreferredOrEqual.side_effect = \
+			lambda b1, b2: b1 == self.bid1
+		self.profile2.isPreferredOrEqual.side_effect = \
+			lambda b1, b2: b1 == self.bid1
+
+		points = self.pareto.getPoints()
+		self.assertEqual(1, len(points))
+		self.assertEqual(self.bid1, next(iter(points)))
+
+	def testgenericParetoTest2(self):
+
+		# both prefer bid2 over any other bid.
+		self.profile1.isPreferredOrEqual.side_effect = \
+			lambda b1, b2: b1 == self.bid2
+		self.profile2.isPreferredOrEqual.side_effect = \
+			lambda b1, b2: b1 == self.bid2
+
+		points = self.pareto.getPoints()
+		# since both prefer bid2, bid is pareto point.
+		self.assertEqual(1, len(points))
+		self.assertEqual(self.bid2, next(iter(points)))
+
+	def testgenericParetoTest3(self):
+		# profile1 prefers bid1, profile2 prefers bid2.
+		# now neither bid1 nor bid2 are dominating each other
+		# and nor does bid1 or bid2 dominate anything else.
+		self.profile1.isPreferredOrEqual.side_effect = \
+			lambda b1, b2: b1 == self.bid1
+		self.profile2.isPreferredOrEqual.side_effect = \
+			lambda b1, b2: b1 == self.bid2
+
+		points = self.pareto.getPoints()
+		# since both prefer bid2, bid is pareto point.
+		self.assertEquals(6, len(points))
+
+	def testgenericParetoTest4(self):
+
+		# profile1 prefers bid1, profile2 prefers bid2.
+		# but they both hate bid3.
+		self.profile1.isPreferredOrEqual.side_effect = \
+			lambda b1, b2: b1 == self.bid1 or b2 == self.bid3
+		self.profile2.isPreferredOrEqual.side_effect = \
+			lambda b1, b2: b2 == self.bid3 or b1 == self.bid2
+
+		points = self.pareto.getPoints()
+		# both hate bid3 but don't agree on bid1/2. bid3 is ruled out.
+		self.assertEqual(5, len(points))
+
+	def testNullNotOk(self):
+		self.assertRaises(ValueError,
+			lambda:GenericPareto([self.profile1, None, self.profile1]))
+
+	def testImmutable(self):
+		list = [self.profile1, self.profile2]
+		pareto = GenericPareto(list)
+		# if we modify the list, the pareto should not change
+		list.append(self.profile3)
+		self.assertEqual(2, len(pareto.getProfiles()))
+
+	# in python we just copy the list so adding should  do nothing
+	def testGetParetoAndAdd(self):
+		list = self.pareto.getProfiles()
+		list.append(self.profile3)
+		self.assertEqual(2, len(self.pareto.getProfiles()))
+
+	def testGetPointsAndAdd(self):
+		self.assertEqual(1, len(self.pareto.getPoints()))
+		list = self.pareto.getPoints()
+		list.add(self.bid1)
+		self.assertEqual(1, len(self.pareto.getPoints()))
Index: /geniuswebcore/test/geniusweb/bidspace/pareto/ParetoE2Etest.py
===================================================================
--- /geniuswebcore/test/geniusweb/bidspace/pareto/ParetoE2Etest.py	(revision 84)
+++ /geniuswebcore/test/geniusweb/bidspace/pareto/ParetoE2Etest.py	(revision 84)
@@ -0,0 +1,55 @@
+import json
+from pathlib import Path
+import unittest
+
+from pyson.ObjectMapper import ObjectMapper
+
+from geniusweb.bidspace.pareto.GenericPareto import GenericPareto
+from geniusweb.bidspace.pareto.ParetoLinearAdditive import ParetoLinearAdditive
+from geniusweb.profile.Profile import Profile
+
+
+class ParetoE2Etest (unittest.TestCase):
+	'''
+	Test pareto functionality with some real profiles in an e2e test
+	'''
+	jackson = ObjectMapper()
+
+	data = \
+		[\
+				[ "test/resources/jobs/jobs1.json", \
+						"test/resources/jobs/jobs2.json", 18 ], \
+				[ "test/resources/7issues/7issues1.json", \
+						"test/resources/7issues/7issues2.json", 557 ]\
+		]
+	
+	def testAll(self):
+		''' 
+		runs all the tests. Python has no standard 'parameterized' mechanism
+		so we have to do it ourselves.
+		'''
+		for [profile1, profile2, expectedNr] in self.data:
+			self.path1 = profile1
+			self.path2 = profile2
+			self.expectedNrPoints = expectedNr
+			with self.subTest("checkGeneric " + self.path1 + "," + self.path2):
+				self.checkGenerciPareto()
+
+	def checkGenerciPareto(self):
+		file1 = Path(self.path1).read_text("utf-8")
+		profile1 = self.jackson.parse(json.loads(file1), Profile)
+		
+		file2 = Path(self.path2).read_text("utf-8")
+		profile2 = self.jackson.parse(json.loads(file2), Profile)
+
+		profiles = [profile1, profile2]
+		linaddpareto = ParetoLinearAdditive(profiles)
+		print("linaddpareto result" + str(linaddpareto))
+		self.assertEqual(self.expectedNrPoints, len(linaddpareto.getPoints()))
+
+		if self.expectedNrPoints < 40:
+			generalpareto = GenericPareto(profiles)
+			self.assertEqual(generalpareto.getPoints(), linaddpareto.getPoints())
+			print("generalpareto result" + str(generalpareto))
+		else:
+			print("Skippedn generalpareto - expected too slow")
Index: /geniuswebcore/test/geniusweb/bidspace/pareto/ParetoPointTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/bidspace/pareto/ParetoPointTest.py	(revision 84)
+++ /geniuswebcore/test/geniusweb/bidspace/pareto/ParetoPointTest.py	(revision 84)
@@ -0,0 +1,88 @@
+from decimal import Decimal
+import unittest
+from unittest.mock import Mock
+
+from geniusweb.bidspace.pareto.ParetoPoint import ParetoPoint
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.issuevalue.Value import Value
+from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
+
+
+class ParetoPointTest(unittest.TestCase):
+	D0_2 = Decimal("0.2")
+	D0_1 = Decimal("0.1")
+	D0_3 = Decimal("0.3")
+	D0_4 = Decimal("0.4")
+	D0_5 = Decimal("0.5")
+	space1 = Mock(LinearAdditive)	
+	space2 = Mock(LinearAdditive)
+	bid11 = Mock(Bid)
+	bid12 = Mock(Bid)
+	bid21 = Mock(Bid)
+	bid22 = Mock(Bid)
+	
+	def setUp(self):
+		self.space1.getUtility = Mock(side_effect=lambda b: \
+			{self.bid11:self.D0_1, self.bid12:self.D0_1, \
+			self.bid21:self.D0_2, self.bid22:self.D0_2 }[b]) 
+		self.space2.getUtility = Mock(side_effect=lambda b: \
+			{self.bid11:self.D0_1, self.bid12:self.D0_2, \
+			self.bid21:self.D0_1, self.bid22:self.D0_2 }[b]) 
+
+		self.point11 = ParetoPoint.create(self.bid11, [self.space1, self.space2])
+		self.point12 = ParetoPoint.create(self.bid12, [self.space1, self.space2])
+		self.point21 = ParetoPoint.create(self.bid21, [self.space1, self.space2])
+		self.point22 = ParetoPoint.create(self.bid22, [self.space1, self.space2])
+
+	def testconstructorTest(self):
+		self.assertEqual(self.bid11, self.point11.getBid())
+		self.assertEqual(self.bid21, self.point21.getBid())
+
+		self.assertEqual([self.D0_1, self.D0_1], self.point11.getUtilities())
+		self.assertEqual([self.D0_2, self.D0_1], self.point21.getUtilities())
+
+	def testdominationTest(self):
+		self.assertTrue(self.point11.isDominatedBy(self.point11))
+		self.assertTrue(self.point11.isDominatedBy(self.point12))
+		self.assertTrue(self.point11.isDominatedBy(self.point21))
+		self.assertTrue(self.point11.isDominatedBy(self.point22))
+
+		self.assertFalse(self.point12.isDominatedBy(self.point11))
+		self.assertFalse(self.point12.isDominatedBy(self.point21))
+		self.assertTrue(self.point12.isDominatedBy(self.point12))
+		self.assertTrue(self.point12.isDominatedBy(self.point22))
+
+		self.assertFalse(self.point21.isDominatedBy(self.point11))
+		self.assertFalse(self.point21.isDominatedBy(self.point12))
+		self.assertTrue(self.point21.isDominatedBy(self.point21))
+		self.assertTrue(self.point21.isDominatedBy(self.point22))
+
+		self.assertFalse(self.point22.isDominatedBy(self.point11))
+		self.assertFalse(self.point22.isDominatedBy(self.point12))
+		self.assertFalse(self.point22.isDominatedBy(self.point21))
+		self.assertTrue(self.point22.isDominatedBy(self.point22))
+
+	def testmergeTest(self):
+		# partial bid 020_010 has value 0.2 for issue 2 in space1 and 0.1 for
+		# issue 2 in space2.
+		value1 = Mock(Value)
+		value2 = Mock(Value)
+		value3 = Mock(Value)
+
+		partialbid020_010 = Bid({"issue2": value2})
+		partialbid102_301 = Bid({"issue1": value1, "issue3":value3})
+		# merged this should be a bid 122_311 so it has utilities
+		# 1+2+2=0.5 in space1 and 3+1+1=0.5 in space2
+
+		self.space1.getUtility = Mock(side_effect=lambda bid:\
+			{partialbid020_010:self.D0_2, partialbid102_301:self.D0_3}[bid])
+		self.space2.getUtility = Mock(side_effect=lambda bid:
+			{partialbid020_010:self.D0_1, partialbid102_301:self.D0_4 }[bid])
+	
+		p1 = ParetoPoint.create(partialbid020_010, [self.space1, self.space2])
+		p2 = ParetoPoint.create(partialbid102_301, [self.space1, self.space2])
+
+		p = p1.merge(p2)
+		bid = p.getBid()
+		self.assertEqual(self.D0_5, p.getUtilities()[0])
+		self.assertEqual(self.D0_5, p.getUtilities()[1])
Index: /geniuswebcore/test/geniusweb/bidspace/pareto/PartialParetoTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/bidspace/pareto/PartialParetoTest.py	(revision 84)
+++ /geniuswebcore/test/geniusweb/bidspace/pareto/PartialParetoTest.py	(revision 84)
@@ -0,0 +1,85 @@
+from decimal import Decimal
+from typing import Set, Dict
+import unittest
+from unittest.mock import Mock
+
+from geniusweb.bidspace.pareto.PartialPareto import PartialPareto
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet
+from geniusweb.issuevalue.Domain import Domain
+from geniusweb.issuevalue.NumberValue import NumberValue
+from geniusweb.issuevalue.NumberValueSet import NumberValueSet
+from geniusweb.issuevalue.Value import Value
+from geniusweb.issuevalue.ValueSet import ValueSet
+from geniusweb.profile.utilityspace.LinearAdditive import LinearAdditive
+
+
+class PartialParetoTest(unittest.TestCase):
+	D0_0 = Decimal(0)
+	D0_2 = Decimal("0.2")
+	D0_1 = Decimal("0.1")
+	D0_3 = Decimal("0.3")
+	D0_4 = Decimal("0.4")
+	D0_5 = Decimal("0.5")
+
+	space1 = Mock(LinearAdditive)
+	space2 = Mock(LinearAdditive)
+
+	domain = Mock(Domain)
+	issues:Set[str] = set(["issue1", "issue2"])
+	value1a = Mock(Value)
+	value1b = Mock(Value)
+	value2a = Mock(Value) 
+	value2b = Mock(Value)
+
+	def setUp(self):
+
+		map:Dict[str, Value] = {}
+		map["issue1"] = self.value1a
+		self.bid10 = Bid(map)
+		map["issue1"] = self.value1b
+		self.bid20 = Bid(map)
+
+		map = {}
+		map["issue2"] = self.value2a
+		self.bid01 = Bid(map)
+		map["issue2"] = self.value2b
+		self.bid02 = Bid(map)
+
+		self.space1.getUtility = Mock(side_effect=lambda b: \
+			{self.bid10:self.D0_1, self.bid20:self.D0_2, \
+			self.bid01:self.D0_0, self.bid02:self.D0_0 }[b]) 
+		self.space2.getUtility = Mock(side_effect=lambda b: \
+			{self.bid10:self.D0_0, self.bid20:self.D0_0, \
+			self.bid01:self.D0_0, self.bid02:self.D0_2 }[b]) 
+
+		self.space1.getDomain = Mock(return_value=self.domain)
+		self.domain.getIssues = Mock(return_value=self.issues)
+		self.issue1valueset = DiscreteValueSet([self.value1a, self.value1b])
+		self.domain.getValues = Mock(side_effect=lambda iss:\
+					{"issue1":self.issue1valueset, "issue2":self.issue2valueset}[iss])
+			
+		# by putting the best value first in the list now,
+		# we may trigger a different evaluation path in the PartialPareto
+		# algorithm, improving coverage
+		self.issue2valueset = DiscreteValueSet([self.value2b, self.value2a])
+
+	def testOneIssueTest(self):
+		pp = PartialPareto.create([self.space1, self.space2], ["issue1"])
+		# bid22 is best for both. Check that's the only remaining point
+		self.assertEqual(1, len(pp.getPoints()))
+		map:Dict[str, Value] = {}
+		map["issue1"] = self.value1b
+		# there is no value for issue2 because we asked to exclude it from the
+		# pareto.
+		self.assertEqual(Bid(map), pp.getPoints()[0].getBid())
+
+	def testTwoIssuesTest(self):
+		pp = PartialPareto.create([self.space1, self.space2],
+				["issue1", "issue2"])
+		# bid22 is best for both. Check that's the only remaining point
+		self.assertEqual(1, len(pp.getPoints()))
+		map:Dict[str, Value] = {}
+		map["issue1"] = self.value1b
+		map["issue2"] = self.value2b
+		self.assertEqual(Bid(map), pp.getPoints()[0].getBid())
Index: /geniuswebcore/test/geniusweb/opponentmodel/FrequencyOppModelTest.py
===================================================================
--- /geniuswebcore/test/geniusweb/opponentmodel/FrequencyOppModelTest.py	(revision 84)
+++ /geniuswebcore/test/geniusweb/opponentmodel/FrequencyOppModelTest.py	(revision 84)
@@ -0,0 +1,193 @@
+from unitpy.GeneralTests import GeneralTests
+from geniusweb.opponentmodel.FrequencyOpponentModel import FrequencyOpponentModel
+from geniusweb.issuevalue.DiscreteValue import DiscreteValue
+from unittest.mock import Mock
+from geniusweb.progress.Progress import Progress
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.actions.Offer import Offer
+from decimal import Decimal
+from typing import Dict, List
+from geniusweb.issuevalue.ValueSet import ValueSet
+from geniusweb.issuevalue.DiscreteValueSet import DiscreteValueSet
+from geniusweb.issuevalue.NumberValueSet import NumberValueSet
+from geniusweb.issuevalue.Domain import Domain
+from geniusweb.issuevalue.Value import Value
+from geniusweb.issuevalue.NumberValue import NumberValue
+from geniusweb.issuevalue.Bid import Bid
+from geniusweb.actions.Offer import Offer
+import unittest
+from tudelft.utilities.immutablelist.Range import Range
+
+class FrequencyOppModelTest (unittest.TestCase, GeneralTests[FrequencyOpponentModel] ):
+	ISS1 = "issue1"
+	ISS2 = "issue2";
+	I1V1 = DiscreteValue("i1v1")
+	I1V2 = DiscreteValue("i1v2");
+	I2V1 = DiscreteValue("i2v1")
+	I2V2 = DiscreteValue("i2v2")
+	I1V2b = DiscreteValue("i1v2b");
+	progress = Mock(Progress);
+	other = PartyId("other")
+	HALF = Decimal("0.5")
+	#list:List[List[Bid]]  = []
+
+	def setUp(self):
+		issues:Dict[str, ValueSet]  = {}
+		discretevalues1:List[DiscreteValue]  = []
+		discretevalues1.append(self.I1V1);
+		discretevalues1.append(self.I1V2);
+		values1 = DiscreteValueSet(discretevalues1)
+		issues[self.ISS1]= values1
+		values2 = NumberValueSet(Range(Decimal(0),Decimal(10), Decimal("0.3")))
+		issues[self.ISS2]=values2
+		self.domain = Domain("test", issues)
+		self.domain2 = Domain("test2", issues)
+
+		# slightly different issue1
+		issues = {}
+		discretevalues1 = []
+		discretevalues1.append(self.I1V1)
+		discretevalues1.append(self.I1V2b)
+		values1 = DiscreteValueSet(discretevalues1)
+		issues[self.ISS1]= values1
+		values2 = NumberValueSet(Range(Decimal(0), Decimal(10),Decimal("0.3")))
+		issues[self.ISS2]= values2
+		self.domain3 = Domain("test", issues)
+
+		# all bids are for domain
+		issuevalues:Dict[str, Value]  = {}
+		issuevalues[self.ISS1]= self.I1V1
+		issuevalues[self.ISS2]=NumberValue(Decimal("1.2"))
+		self.bid1 = Bid(issuevalues)
+
+		issuevalues[self.ISS1]= self.I1V1
+		issuevalues[self.ISS2]= NumberValue(Decimal("1.5"))
+		self.bid2 = Bid(issuevalues)
+
+		issuevalues[self.ISS1]= self.I1V2
+		issuevalues[self.ISS2]= NumberValue(Decimal("1.5"))
+		self.bid3 = Bid(issuevalues)
+
+		self.oppModel1 = FrequencyOpponentModel.create().With(self.domain, None)
+		self.oppModel1b = FrequencyOpponentModel.create().With(self.domain, None)
+		self.oppModel2 = FrequencyOpponentModel.create().With(self.domain2, None)
+		self.oppModel3 = FrequencyOpponentModel.create().With(self.domain3, None)
+		self.oppModel4 = self.oppModel3.WithAction(Offer(self.other, self.bid1), self.progress)
+
+	#Override
+	def getGeneralTestData(self) -> List[List[FrequencyOpponentModel]] :
+		return [[self.oppModel1, self.oppModel1b],
+				[self.oppModel2], [self.oppModel3],	[self.oppModel4]]
+
+	#Override
+	def getGeneralTestStrings(self)->List[str] :
+		return ["FrequencyOpponentModel\\[0,\\{issue.=\\{\\}, issue.=\\{\\}\\}\\]",
+				"FrequencyOpponentModel\\[0,\\{issue.=\\{\\}, issue.=\\{\\}\\}\\]",
+				"FrequencyOpponentModel\\[0,\\{issue.=\\{\\}, issue.=\\{\\}\\}\\]",
+				"FrequencyOpponentModel\\[1,\\{issue2=\\{1.2=1\\}, issue1=\\{\"i1v1\"=1\\}\\}\\]"]
+
+	def testsmokeTestNull(self) :
+		self.assertRaises(ValueError, lambda:FrequencyOpponentModel.create().With(None,None))
+
+	def testsmokeTest(self):
+		FrequencyOpponentModel.create().With(self.domain, None)
+
+	def testEmptyModel(self):
+		oppModel = FrequencyOpponentModel.create().With(self.domain, None)
+		self.assertEqual(1, oppModel.getUtility(self.bid1))
+		self.assertEqual(1, oppModel.getUtility(self.bid2))
+		
+	def testEmptyModelPartialBid(self):
+		oppModel =FrequencyOpponentModel.create().With(self.domain, None)
+		bid = Bid({})
+		self.assertEqual(1, oppModel.getUtility(bid))
+
+	def testPartialUtility(self):
+		freqs:Dict[str, Dict[Value, int]] = {}
+		freqs1:Dict[Value, int]  = {}
+		freqs2:Dict[Value, int]  = {}
+
+		freqs1[self.I1V1]= 2
+		freqs1[self.I1V2]= 0
+		freqs2[self.I2V1]= 1
+		freqs2[self.I2V2]= 1
+
+		freqs[self.ISS1]= freqs1
+		freqs[self.ISS2]= freqs2
+		total = Decimal(2)
+
+		oppModel = FrequencyOpponentModel(self.domain,	freqs, total,None)
+
+		E = 0.0000001
+		self.assertAlmostEqual(0.5, oppModel.getUtility(Bid({self.ISS1: self.I1V1})), None,E)
+		self.assertAlmostEqual(0, oppModel.getUtility(Bid({self.ISS1: self.I1V2})))
+		self.assertAlmostEqual(0.25,
+				oppModel.getUtility(Bid({self.ISS2:self.I2V1})), None,E)
+		self.assertAlmostEqual(0.25, oppModel.getUtility(Bid({self.ISS2: self.I2V2})))
+
+
+	def testUpdate(self):
+		oppModel = self.oppModel1.WithAction(Offer(self.other, self.bid1),
+				self.progress);
+		self.assertEqual(1, oppModel.getUtility(self.bid1) )
+		self.assertEqual(0,oppModel.getUtility(self.bid3) )
+		# bid2 has 1 of 2 issue values same as bid1.
+		self.assertTrue(0.5, oppModel.getUtility(self.bid2))
+
+	def testUpdate2(self):
+		# bid1 and bid2 both want I1V1. They differ on the number value.
+		# bid3 wants I1V2 but does have the number value from bid2
+		oppModel = self.oppModel1\
+				.WithAction(Offer(self.other, self.bid1), self.progress)\
+				.WithAction(Offer(self.other, self.bid2), self.progress)
+		self.assertEqual(0.75, oppModel.getUtility(self.bid1))
+		self.assertEqual(0.75, oppModel.getUtility(self.bid2))
+		self.assertEqual(0.25, oppModel.getUtility(self.bid3)) 
+
+	def testPartialBidUpdate(self):
+		oppModel = self.oppModel1.WithAction(Offer(self.other, self.bid1),
+				self.progress)
+		partialbid = Bid({self.ISS1: self.I1V1})
+		oppModel.WithAction(Offer(self.other, partialbid), self.progress)
+
+	def testGetCounts(self):
+		self.assertEqual({}, self.oppModel1.getCounts(self.ISS1))
+		self.assertEquals({}, self.oppModel1.getCounts(self.ISS2))
+
+		values1:Dict[str, Value] = {}
+		values1[self.ISS1]= self.I1V1
+		values1[self.ISS2]= self.I2V1
+		offer1 = Offer(self.other, Bid(values1))
+
+		oppmod = self.oppModel1.WithAction(offer1, self.progress)
+
+		self.assertEqual(1, oppmod.getCounts(self.ISS1)[self.I1V1])
+		self.assertFalse(self.I1V2 in oppmod.getCounts(self.ISS1))
+		self.assertEquals(1, oppmod.getCounts(self.ISS2)[self.I2V1])
+		self.assertFalse(self.I2V2 in oppmod.getCounts(self.ISS2))
+
+		# same identical bid. Counts should go to 2
+		oppmod = oppmod.WithAction(offer1, self.progress)
+
+		self.assertEquals(2, oppmod.getCounts(self.ISS1)[self.I1V1])
+		self.assertEquals(2, oppmod.getCounts(self.ISS2)[self.I2V1])
+
+		values2:Dict[str, Value] = {}
+		values2[self.ISS1]=self.I1V2
+		values2[self.ISS2]=self.I2V2
+		offer2 = Offer(self.other, Bid(values2))
+
+		# Other bid. with value2 for both issues
+		oppmod = oppmod.WithAction(offer2, self.progress)
+
+		self.assertEqual(2, oppmod.getCounts(self.ISS1)[self.I1V1])
+		self.assertEqual(2, oppmod.getCounts(self.ISS2)[self.I2V1])
+		self.assertEqual(1, oppmod.getCounts(self.ISS1)[self.I1V2])
+		self.assertEqual(1, oppmod.getCounts(self.ISS2)[self.I2V2])
+
+
+	def testStableName(self):
+		name = self.oppModel1.getName()
+		self.assertNotEqual(None, name)
+		self.assertEqual(name, self.oppModel1.getName())
+		
Index: /geniuswebcore/test/resources/7issues/7issues1.json
===================================================================
--- /geniuswebcore/test/resources/7issues/7issues1.json	(revision 83)
+++ /geniuswebcore/test/resources/7issues/7issues1.json	(revision 84)
@@ -9,5 +9,4 @@
 					"highValue": 9,
 					"highUtility": 0.99
-
 				}
 			},
@@ -18,5 +17,4 @@
 					"highValue": 9,
 					"highUtility": 0.97
-
 				}
 			},
@@ -27,5 +25,4 @@
 					"highValue": 9,
 					"highUtility": 0.96
-
 				}
 			},
@@ -36,5 +33,4 @@
 					"highValue": 9,
 					"highUtility": 0.94
-
 				}
 			},
@@ -45,5 +41,4 @@
 					"highValue": 9,
 					"highUtility": 0.93
-
 				}
 			},
@@ -54,5 +49,4 @@
 					"highValue": 9,
 					"highUtility": 0.92
-
 				}
 			},
@@ -63,5 +57,4 @@
 					"highValue": 9,
 					"highUtility": 0.91
-
 				}
 			}
@@ -80,23 +73,23 @@
 			"issuesValues": {
 				"i1": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i2": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i3": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i4": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i5": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i6": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i7": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				}
 			}
Index: /geniuswebcore/test/resources/7issues/7issues2.json
===================================================================
--- /geniuswebcore/test/resources/7issues/7issues2.json	(revision 83)
+++ /geniuswebcore/test/resources/7issues/7issues2.json	(revision 84)
@@ -9,5 +9,4 @@
 					"highValue": 9,
 					"highUtility": 0.11
-
 				}
 			},
@@ -18,5 +17,4 @@
 					"highValue": 9,
 					"highUtility": 0.21
-
 				}
 			},
@@ -27,5 +25,4 @@
 					"highValue": 9,
 					"highUtility": 0.13
-
 				}
 			},
@@ -36,5 +33,4 @@
 					"highValue": 9,
 					"highUtility": 0.14
-
 				}
 			},
@@ -45,5 +41,4 @@
 					"highValue": 9,
 					"highUtility": 0.15
-
 				}
 			},
@@ -54,5 +49,4 @@
 					"highValue": 9,
 					"highUtility": 0.16
-
 				}
 			},
@@ -63,16 +57,15 @@
 					"highValue": 9,
 					"highUtility": 0.17
-
 				}
 			}
 		},
 		"issueWeights": {
-			"i1": 0.24745759773026107,
-			"i2": 0.11824193959921694,
-			"i3": 0.1860471425098606,
-			"i4": 0.20629555826677742,
-			"i5": 0.16112666189388397,
+			"i1": 0.24746,
+			"i2": 0.118241,
+			"i3": 0.186047,
+			"i4": 0.206295,
+			"i5": 0.161126,
 			"i6": 0.0538874,
-			"i7": 0.0269437
+			"i7": 0.0269436
 		},
 		"domain": {
@@ -80,23 +73,23 @@
 			"issuesValues": {
 				"i1": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i2": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i3": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i4": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i5": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i6": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				},
 				"i7": {
-					"range": {"low":0,"high":9,"step":1}
+					"range": {"low":0,"high":9,"step":1} 
 				}
 			}
