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}
+		'''
