Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ lazy val commonSettings = Seq(
organizationHomepage := Some(url("https://economicsl.github.io/")),
libraryDependencies ++= Seq(
"org.scalactic" %% "scalactic" % "3.0.5",
"org.typelevel" %% "cats-core" % "1.1.0",
"com.typesafe.akka" %% "akka-actor" % "2.5.6",
"org.economicsl" %% "esl-core" % "0.1.0-SNAPSHOT"
),
Expand All @@ -22,7 +23,8 @@ lazy val commonSettings = Seq(
"-language:reflectiveCalls", // needed in order to enable structural (or duck) typing
"-Xlint",
"-Ywarn-unused-import",
"-Ywarn-dead-code"
"-Ywarn-dead-code",
"-Ypartial-unification"
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import java.util.UUID
import org.economicsl.core.Price


case class HouseListing(uuid: UUID, issuer: Long, price: Price, house: House)
case class HouseListing(uuid: UUID, issuer: UUID, price: Price, house: House)
extends Predicate[HousePurchaseOffer] with Preferences[HousePurchaseOffer] {

/** Boolean function used to determine whether some `HousePurchaseOffer` is acceptable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,64 +15,64 @@ limitations under the License.
*/
package org.economicsl.matching

import cats.data.State
import org.economicsl.core.Price
import org.economicsl.matching.onetoone.DeferredAcceptanceAlgorithm
import org.scalacheck.{Gen, Prop, Properties}

import scala.collection.immutable.HashSet


object HouseMarketSpecification extends Properties("housing-market") {

// this generator should exist in esl-core!
val priceGen: Gen[Price] = {
def price: Gen[Price] = {
for {
value <- Gen.choose(100000, 1000000)
value <- Gen.posNum[Long]
} yield Price(value)
}

val houseGen: Gen[House] = {
val house: Gen[House] = {
for {
id <- Gen.uuid
quality <- Gen.choose(1, 100)
} yield House(id, quality)
uuid <- Gen.uuid
quality <- Gen.posNum[Int]
} yield House(uuid, quality)
}

val houseListingGen: Gen[HouseListing] = {
val houseListing: Gen[HouseListing] = {
for {
id <- Gen.uuid
issuer <- Gen.posNum[Long]
price <- priceGen
house <- houseGen
} yield HouseListing(id, issuer, price, house)
uuid <- Gen.uuid
issuer <- Gen.uuid
price <- price
house <- house
} yield HouseListing(uuid, issuer, price, house)
}

val housePurchaseOfferGen: Gen[HousePurchaseOffer] = {
val housePurchaseOffer: Gen[HousePurchaseOffer] = {
for {
id <- Gen.uuid
issuer <- Gen.posNum[Long]
price <- priceGen
} yield HousePurchaseOffer(id, issuer, price)
uuid <- Gen.uuid
issuer <- Gen.uuid
price <- price
} yield HousePurchaseOffer(uuid, issuer, price)
}

val housePurchaseOffers: Gen[HashSet[HousePurchaseOffer]] = Gen.sized {
n => Gen.containerOfN[HashSet, HousePurchaseOffer](n, housePurchaseOfferGen)
}
def unMatched: Gen[(Set[HousePurchaseOffer], Set[HouseListing])] = Gen.sized {
size => for {
offers <- Gen.containerOfN[Set, HousePurchaseOffer](size, housePurchaseOffer)
listing <- Gen.containerOfN[Set, HouseListing](size, houseListing)
} yield (offers, listing)

val houseListings: Gen[HashSet[HouseListing]] = Gen.sized {
n => Gen.containerOfN[HashSet, HouseListing](n, houseListingGen)
}

property("matching should be incentive-compatible") = Prop.forAll(housePurchaseOffers, houseListings) {
case (offers, listings) =>
val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing])(offers, listings)
matching.matches.forall{ case (offer, listing) => offer.price >= listing.price }
property("matching should be incentive-compatible") = Prop.forAll(unMatched) { unmatched =>
val result = State(new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing]).run(unmatched)
val ((_, _), matching) = result.value
matching.matches.forall{ case (offer, listing) => offer.price >= listing.price }
}

property("matching should be stable") = Prop.forAll(housePurchaseOffers, houseListings) {
case (offers, listings) =>
val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing])(offers, listings)
offers.forall(o => listings.forall(l => !matching.isBlockedBy(l -> o)))
property("matching should be stable") = Prop.forAll(unMatched) { unmatched =>
val result = State(new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing]).run(unmatched)
val ((_, _), matching) = result.value
val (offers, listings) = unmatched
offers.forall(o => listings.forall(l => !matching.isBlockedBy(l -> o)))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import java.util.UUID
import org.economicsl.core.Price


case class HousePurchaseOffer(uuid: UUID, issuer: Long, price: Price)
case class HousePurchaseOffer(uuid: UUID, issuer: UUID, price: Price)
extends Proposer with Predicate[HouseListing] with Preferences[HouseListing] {

/** Boolean function used to determine whether some `HouseListing` is acceptable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,67 +27,68 @@ import scala.collection.immutable.TreeSet
*/
class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferences[W],
W <: Predicate[M] with Preferences[M] with Quota]
extends ((Set[M], Set[W]) => ((Set[M], Set[W]), ManyToOneMatching[W, M])) {
extends (((Set[M], Set[W])) => ((Set[M], Set[W]), ManyToOneMatching[W, M])) {

/** Compute a stable matching between two sets.
*
* @param ms set of proposers to be matched.
* @param ws set of proposees to be matched.
* @return
* @note A matching will be called "stable" unless there is a pair each of which strictly prefers the other
* to its partner in the matching. This algorithm produces a weakly stable matching in `O(n^2)` time where `n`
* is the size of the inputs sets.
*/
def apply(ms: Set[M], ws: Set[W]): ((Set[M], Set[W]), ManyToOneMatching[W, M]) = {
/** Compute a stable matching between two sets.
*
* @param unmatched
* @return
* @note A matching will be called "stable" unless there is a pair each of which strictly prefers the other
* to its partner in the matching. This algorithm produces a weakly stable matching in `O(n^2)` time where `n`
* is the size of the inputs sets.
*/
def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), ManyToOneMatching[W, M]) = {
val (ms, ws) = unmatched

@annotation.tailrec
def accumulate(unMatched: Set[M], toBeMatched: Set[M], matches: Map[W, TreeSet[M]], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, TreeSet[M]]) = {
toBeMatched.headOption match {
case Some(toBeMatchedM) =>
val previouslyRejected = rejected.getOrElse(toBeMatchedM, Set.empty)
val acceptableWs = ws.diff(previouslyRejected)
if (acceptableWs.isEmpty) {
accumulate(unMatched + toBeMatchedM, toBeMatched - toBeMatchedM, matches, rejected)
} else {
val mostPreferredW = acceptableWs.max(toBeMatchedM.ordering)
matches.get(mostPreferredW) match {
case Some(matchedMs) if mostPreferredW.isAcceptable(toBeMatchedM) =>
matchedMs.headOption match {
case Some(_) if matchedMs.size < mostPreferredW.quota =>
val updatedToBeMatchedMs = toBeMatched - toBeMatchedM
val updatedMatches = matches.updated(mostPreferredW, matchedMs + toBeMatchedM)
accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, rejected)
case Some(leastPreferredMatchedM) if mostPreferredW.ordering.lt(leastPreferredMatchedM, toBeMatchedM) =>
val updatedToBeMatchedMs = toBeMatched - toBeMatchedM + leastPreferredMatchedM
val updatedMatches = matches.updated(mostPreferredW, matchedMs - leastPreferredMatchedM + toBeMatchedM)
val updatedRejected = rejected.updated(leastPreferredMatchedM, rejected.getOrElse(leastPreferredMatchedM, Set.empty) + mostPreferredW)
accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, updatedRejected)
case Some(leastPreferredMatchedM) if mostPreferredW.ordering.gteq(leastPreferredMatchedM, toBeMatchedM) =>
val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW)
accumulate(unMatched, toBeMatched, matches, updatedRejected)
case None =>
val updatedToBeMatchedMs = toBeMatched - toBeMatchedM
val updatedMatches = matches.updated(mostPreferredW, matchedMs + toBeMatchedM)
accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, rejected)
}
case Some(_) if mostPreferredW.isNotAcceptable(toBeMatchedM) =>
val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW)
accumulate(unMatched, toBeMatched, matches, updatedRejected)
case None if mostPreferredW.isAcceptable(toBeMatchedM) =>
val updatedMatches = matches + (mostPreferredW -> TreeSet(toBeMatchedM)(mostPreferredW.ordering))
accumulate(unMatched, toBeMatched - toBeMatchedM, updatedMatches, rejected)
case None if mostPreferredW.isNotAcceptable(toBeMatchedM) =>
val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW)
accumulate(unMatched, toBeMatched, matches, updatedRejected)
}
@annotation.tailrec
def accumulate(unMatched: Set[M], toBeMatched: Set[M], matches: Map[W, TreeSet[M]], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, TreeSet[M]]) = {
toBeMatched.headOption match {
case Some(toBeMatchedM) =>
val previouslyRejected = rejected.getOrElse(toBeMatchedM, Set.empty)
val acceptableWs = ws.diff(previouslyRejected)
if (acceptableWs.isEmpty) {
accumulate(unMatched + toBeMatchedM, toBeMatched - toBeMatchedM, matches, rejected)
} else {
val mostPreferredW = acceptableWs.max(toBeMatchedM.ordering)
matches.get(mostPreferredW) match {
case Some(matchedMs) if mostPreferredW.isAcceptable(toBeMatchedM) =>
matchedMs.headOption match {
case Some(_) if matchedMs.size < mostPreferredW.quota =>
val updatedToBeMatchedMs = toBeMatched - toBeMatchedM
val updatedMatches = matches.updated(mostPreferredW, matchedMs + toBeMatchedM)
accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, rejected)
case Some(leastPreferredMatchedM) if mostPreferredW.ordering.lt(leastPreferredMatchedM, toBeMatchedM) =>
val updatedToBeMatchedMs = toBeMatched - toBeMatchedM + leastPreferredMatchedM
val updatedMatches = matches.updated(mostPreferredW, matchedMs - leastPreferredMatchedM + toBeMatchedM)
val updatedRejected = rejected.updated(leastPreferredMatchedM, rejected.getOrElse(leastPreferredMatchedM, Set.empty) + mostPreferredW)
accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, updatedRejected)
case Some(leastPreferredMatchedM) if mostPreferredW.ordering.gteq(leastPreferredMatchedM, toBeMatchedM) =>
val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW)
accumulate(unMatched, toBeMatched, matches, updatedRejected)
case None =>
val updatedToBeMatchedMs = toBeMatched - toBeMatchedM
val updatedMatches = matches.updated(mostPreferredW, matchedMs + toBeMatchedM)
accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, rejected)
}
case Some(_) if mostPreferredW.isNotAcceptable(toBeMatchedM) =>
val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW)
accumulate(unMatched, toBeMatched, matches, updatedRejected)
case None if mostPreferredW.isAcceptable(toBeMatchedM) =>
val updatedMatches = matches + (mostPreferredW -> TreeSet(toBeMatchedM)(mostPreferredW.ordering))
accumulate(unMatched, toBeMatched - toBeMatchedM, updatedMatches, rejected)
case None if mostPreferredW.isNotAcceptable(toBeMatchedM) =>
val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW)
accumulate(unMatched, toBeMatched, matches, updatedRejected)
}
case None =>
(unMatched, matches.keySet.diff(ws), matches)
}
}
case None =>
(unMatched, matches.keySet.diff(ws), matches)
}
val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable)))
val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, ms, Map.empty, unacceptableWs)
((unMatchedMs, unMatchedWs), ManyToOneMatching(matches))
}

val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable)))
val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, ms, Map.empty, unacceptableWs)
((unMatchedMs, unMatchedWs), ManyToOneMatching(matches))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,25 @@ package org.economicsl.matching.onetoone

import org.economicsl.matching.{Predicate, Preferences, Proposer}


/** Class implementing the deferred acceptance algorithm.
*
* @tparam M the type of proposer.
* @tparam W the type of proposee.
*/
class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferences[W], W <: Predicate[M] with Preferences[M]]
extends ((Set[M], Set[W]) => ((Set[M], Set[W]), OneToOneMatching[W, M])) {
extends (((Set[M], Set[W])) => ((Set[M], Set[W]), OneToOneMatching[W, M])) {

/** Compute a weakly stable matching between two sets.
*
* @param ms set of proposers to be matched.
* @param ws set of proposees to be matched.
* @param unmatched
* @return
* @note A matching will be called "weakly stable" unless there is a pair each of which strictly prefers the other
* to its partner in the matching. This algorithm produces a weakly stable matching in `O(n^2)` time where `n`
* is the size of the inputs sets.
*/
def apply(ms: Set[M], ws: Set[W]): ((Set[M], Set[W]), OneToOneMatching[W, M]) = {
def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), OneToOneMatching[W, M]) = {
val (ms, ws) = unmatched

@annotation.tailrec
def accumulate(unMatchedMs: Set[M], toBeMatchedMs: Set[M], matches: Map[W, M], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, M]) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ import scala.util.{Failure, Success, Try}
* [[http://www.eecs.harvard.edu/cs286r/courses/fall09/papers/galeshapley.pdf ''Gale and Shapley (1962)'']].
*/
class StableMarriageAlgorithm[M <: Proposer with Preferences[W], W <: Preferences[M]]
extends ((Set[M], Set[W]) => Try[((Set[M], Set[W]), OneToOneMatching[W, M])]) {
extends (((Set[M], Set[W])) => ((Set[M], Set[W]), Try[OneToOneMatching[W, M]])) {

/** Compute a stable matching between two sets of equal size.
*
* @param ms set of proposers to be matched.
* @param ws set of proposees to be matched.
* @param unmatched
* @return a stable matching between proposees (`ws`) and proposers (`ms`).
*/
def apply(ms: Set[M], ws: Set[W]): Try[((Set[M], Set[W]), OneToOneMatching[W, M])] = {
def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), Try[OneToOneMatching[W, M]]) = {
val (ms, ws) = unmatched

@annotation.tailrec
def accumulate(unMatchedMs: Set[M], matches: Map[W, M], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, M]) = {
Expand Down Expand Up @@ -74,10 +74,10 @@ class StableMarriageAlgorithm[M <: Proposer with Preferences[W], W <: Preference

if (ms.size == ws.size) {
val (unMatchedMs, unMatchedWs, matches) = accumulate(ms, Map.empty, Map.empty)
Success(((unMatchedMs, unMatchedWs), OneToOneMatching(matches)))
((unMatchedMs, unMatchedWs), Success(OneToOneMatching(matches)))
}
else {
Failure(new IllegalArgumentException(s"The size of ms is ${ms.size} which does not equal the size of ws which is ${ws.size}!"))
((ms, ws), Failure(new IllegalArgumentException(s"The size of ms is ${ms.size} which does not equal the size of ws which is ${ws.size}!")))
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ object DeferredAcceptanceMicroBenchmark extends Bench.OnlineRegressionReport {
}

def randomHouseListing(): HouseListing = {
HouseListing(UUID.randomUUID(), Random.nextLong(), Price(Random.nextLong()), randomHouse())
HouseListing(UUID.randomUUID(), UUID.randomUUID(), Price(Random.nextLong()), randomHouse())
}

def randomHousePurchaseOffer(): HousePurchaseOffer = {
HousePurchaseOffer(UUID.randomUUID(), Random.nextLong(), Price(Random.nextLong()))
HousePurchaseOffer(UUID.randomUUID(), UUID.randomUUID(), Price(Random.nextLong()))
}

def randomHousePurchaseOffers(size: Int): HashSet[HousePurchaseOffer] = {
Expand Down Expand Up @@ -66,8 +66,8 @@ object DeferredAcceptanceMicroBenchmark extends Bench.OnlineRegressionReport {
// }

measure method "weaklyStableMatching" in {
using(unMatchedParticipants) in { case (buyers, sellers) =>
(new StableMarriageAlgorithm[HousePurchaseOffer, HouseListing]())(buyers, sellers)
using(unMatchedParticipants) in { unmatched =>
(new StableMarriageAlgorithm[HousePurchaseOffer, HouseListing]())(unmatched)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ limitations under the License.
*/
package org.economicsl.matching.manytoone

import cats.data.State
import org.scalacheck.{Gen, Prop, Properties}


Expand Down Expand Up @@ -45,14 +46,17 @@ object SchoolChoiceSpecification extends Properties("school-choice") {
}

property("no school accepts more students than its quota allows") = Prop.forAll(unMatched) {
case (students, schools) =>
val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[Student, School])(students, schools)
unmatched =>
val result = State(new DeferredAcceptanceAlgorithm[Student, School]).run(unmatched)
val ((_, _), matching)= result.value
matching.matches.forall{ case (school, matchedStudents) => matchedStudents.size <= school.quota }
}

property("matching should be stable") = Prop.forAll(unMatched) {
case (students, schools) =>
val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[Student, School])(students, schools)
unmatched =>
val result = State(new DeferredAcceptanceAlgorithm[Student, School]).run(unmatched)
val ((_, _), matching)= result.value
val (students, schools) = unmatched
students.forall(student => schools.forall(school => !matching.isBlockedBy(school -> student)))
}

Expand Down
Loading