{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module ProductMixAuction.LP.Rationing
(
RationingStrategy
, rationNoRationing
, rationLinearDemand
, rationLinearDemandPreferPairedBids
, RationingMode(..)
, RationingSteps(..)
, RationingOptions(..)
, noRationing
, linearDemand
, linearDemandPreferPairedBids
, runRationingOptions
, goodAllocations
, bidderAllocations
, bidAllocations
, filterAllocs
, ro_mode
, ro_steps
) where
import Control.Lens
import Data.Aeson.Types as A
import Data.Default.Class
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.List
import Data.Maybe
import GHC.Generics
import ProductMixAuction.LP.AdditionalConstraint
import ProductMixAuction.LP.Bid
import ProductMixAuction.LP.Core
import ProductMixAuction.LP.Demand
import ProductMixAuction.Supply
import ProductMixAuction.Types
goodAllocations :: ScaleFactor -> Map.Map bid (Map.Map Good Units)
-> Map.Map Good Units
goodAllocations sf = Map.unionsWith (+) . Map.elems . Map.map (roundMap sf)
bidderAllocations :: Ord bidder
=> ScaleFactor -> Map.Map (bidder, bid) (Map.Map Good Units)
-> Map.Map bidder (Map.Map Good Units)
bidderAllocations sf = filterAllocs . Map.mapKeysWith (Map.unionWith (+)) fst . Map.map (roundMap sf)
bidAllocations :: Ord bid
=> ScaleFactor -> Map.Map bid (Map.Map Good Units)
-> Map.Map bid (Map.Map Good Units)
bidAllocations sf = filterAllocs . Map.map (roundMap sf)
filterAllocs :: Ord bid
=> Map.Map bid (Map.Map Good Units) -> Map.Map bid (Map.Map Good Units)
filterAllocs = Map.filter (not . Map.null) . Map.map (Map.filter (> 0))
roundMap :: ScaleFactor -> Map.Map x Units -> Map.Map x Units
roundMap sf = Map.map (roundToScale sf)
varsToAllocs :: Ord bid
=> Auction bid -> Map.Map (AVar bid) Units -> Map.Map bid (Map.Map Good Units)
varsToAllocs auction vs = Map.fromList (map bidAllocation bids)
where
bids = auctionBidLabels auction
goods = auctionGoodLabels auction
bidAllocation bid = (bid, Map.fromList [ (good, fromMaybe 0 (Map.lookup (AllocVar bid good) vs))
| good <- goods ])
type RationingStrategy bid =
Auction bid -> AuctionResult bid -> IO (Map.Map bid (Map.Map Good Units))
data RationingMode
= LinearDemandPreferPairedBids
| LinearDemand
| NoRationing
deriving (Eq, Generic, Show)
instance Default RationingMode where def = LinearDemandPreferPairedBids
instance ToJSON RationingMode where toJSON = genericToJSON $ jsonOptions ""
instance FromJSON RationingMode where parseJSON = genericParseJSON $ jsonOptions ""
newtype RationingSteps = RationingSteps { _RationingSteps :: Int }
deriving (Eq, Ord, Num, Generic, Show, ToJSON, FromJSON)
data RationingOptions = RationingOptions
{ _ro_mode :: RationingMode
, _ro_steps :: Maybe RationingSteps
}
deriving (Eq, Generic, Show)
noRationing :: RationingOptions
noRationing = RationingOptions NoRationing Nothing
linearDemand :: Maybe RationingSteps -> RationingOptions
linearDemand = RationingOptions LinearDemand
linearDemandPreferPairedBids :: Maybe RationingSteps -> RationingOptions
linearDemandPreferPairedBids = RationingOptions LinearDemandPreferPairedBids
instance Default RationingOptions where def = RationingOptions def (Just def)
instance ToJSON RationingOptions where toJSON = genericToJSON $ jsonOptions "_ro_"
instance FromJSON RationingOptions where parseJSON = genericParseJSON $ jsonOptions "_ro_"
instance Default RationingSteps
runRationingOptions :: Goodly bid => RationingOptions -> RationingStrategy bid
runRationingOptions (RationingOptions k m) =
case k of
NoRationing -> rationNoRationing
LinearDemand -> rationLinearDemand m Nothing
LinearDemandPreferPairedBids -> rationLinearDemandPreferPairedBids m Nothing
rationNoRationing :: Ord bid => RationingStrategy bid
rationNoRationing auction ar =
return $ varsToAllocs auction (ar ^. ar_quantities)
rationLinearDemand :: Goodly bid => Maybe RationingSteps -> Maybe (Map.Map Good Int) -> RationingStrategy bid
rationLinearDemand = rationD_or_Di False
rationLinearDemandPreferPairedBids :: Goodly bid => Maybe RationingSteps -> Maybe (Map.Map Good Int) -> RationingStrategy bid
rationLinearDemandPreferPairedBids = rationD_or_Di True
rationD_or_Di :: forall bid . Goodly bid
=> Bool -> Maybe RationingSteps -> Maybe (Map.Map Good Int)
-> RationingStrategy bid
rationD_or_Di prefer_paired_bids mb_steps mb_weights auction ar
| needs_rationing = do ar' <- runAuctionCore "ration" auction'
return $ share_out (joinBidAllocations (ar' ^. ar_quantities))
| otherwise = return $ share_out (ar ^. ar_quantities)
where
ps = ar ^. ar_prices
steps = case mb_steps of
Just s | s > 0 -> s
_ -> defaultSteps prefer_paired_bids ps auction
weights = fromMaybe default_weights mb_weights
default_weights = Map.fromList (zip (auctionGoodLabels auction) [1..])
auction' = fixSupply (ar ^. ar_quantities) (approximateBids prefer_paired_bids steps ps weights auction)
bs = Map.fromList [ (bid ^. bid_label, bid) | bid <- _auction_bids auction ]
share_out :: Map.Map (AVar bid) Units -> Map.Map bid (Map.Map Good Units)
share_out = shareMatching auction . varsToAllocs auction . redistribute
redistribute | prefer_paired_bids = evenlyRedistribute bs ps
| otherwise = id
needs_rationing
| prefer_paired_bids = multiply_marginal > 1 || (multiply_marginal == 1 && singly_marginal > 0)
| otherwise = singly_marginal + multiply_marginal > 1
(singly_marginal, multiply_marginal) = countMarginalBids ps (_auction_bids auction)
fixSupply :: Map.Map (AVar bid) Units -> Auction bid' -> Auction bid'
fixSupply zs auction = auction & auction_supply .~ supply
& auction_preferences .~ []
& auction_tweak_supply .~ False
where
supply = mkFixedSupply [ (good, totalFor good) | good <- auctionGoodLabels auction ]
totalFor good = sum [ roundToScale sf u | (AllocVar _ good', u) <- Map.toList zs, good == good' ]
sf = auction ^. auction_scale
defaultSteps :: Bool -> Map.Map Good Price -> Auction bid -> RationingSteps
defaultSteps prefer_paired_bids ps auction = RationingSteps (1 + ceiling (_Units max_k) * 2 * 10 ^ rho)
where
max_k = maximum (0 : map _bid_quantity (filter (isMarginal prefer_paired_bids ps) (_auction_bids auction)))
ScaleFactor rho = _auction_scale auction
approximateBids :: Ord bid
=> Bool -> RationingSteps -> Map.Map Good Price -> Map.Map Good Int
-> Auction bid -> Auction (bid, Int)
approximateBids prefer_paired_bids (RationingSteps steps) ps weights auction =
auction { _auction_bids = concatMap approx (_auction_bids auction)
, _auction_constraints = map transformConstraint (_auction_constraints auction)
}
where
approx bid = case marginalGoods prefer_paired_bids ps bid of
Nothing -> [bid & bid_label %~ flip (,) 0]
Just gs -> map (toStep bid gs) [0..steps-1]
toStep bid gs l = Bid { _bid_label = (bid ^. bid_label, l)
, _bid_name = bid ^. bid_name
, _bid_quantity = bid ^. bid_quantity / fromIntegral steps
, _bid_values = Map.mapWithKey (f gs l) (bid ^. bid_values)
}
f gs l good bv
| good `Set.member` gs = bv & bv_price +~ fromIntegral (l * weight good) / recip_delta_over_d
& bv_quantity //~ fromIntegral steps
| otherwise = bv & bv_quantity //~ fromIntegral steps
recip_delta_over_d = fromIntegral (1 + dim * steps)
dim = Map.size ps
transformConstraint ac = ac & ac_coefficients %~ Map.fromList . concatMap transformCoefficient . Map.toList
transformCoefficient (b, gs) = case lookupBid b auction of
Just bid | isMarginal prefer_paired_bids ps bid -> [ ((b, l), gs) | l <- [0..steps-1] ]
_ -> [((b, 0), gs)]
weight good = fromMaybe 0 (Map.lookup good weights)
data Marginality = Unsuccessful | NonMarginal | SinglyMarginal Good | MultiplyMarginal (Set.Set Good)
countMarginalBids :: Map.Map Good Price -> [Bid bid] -> (Int, Int)
countMarginalBids ps = foldl' f (0, 0)
where
f c@(!singly_marginal, !multiply_marginal) bid = case bidMarginality ps bid of
SinglyMarginal{} -> (singly_marginal+1, multiply_marginal)
MultiplyMarginal{} -> (singly_marginal, multiply_marginal+1)
_ -> c
isMarginal :: Bool -> Map.Map Good Price -> Bid bid -> Bool
isMarginal prefer_paired_bids ps bid = isJust (marginalGoods prefer_paired_bids ps bid)
marginalGoods :: Bool -> Map.Map Good Price -> Bid bid -> Maybe (Set.Set Good)
marginalGoods prefer_paired_bids ps bid = case bidMarginality ps bid of
Unsuccessful -> Nothing
NonMarginal -> Nothing
SinglyMarginal g | prefer_paired_bids -> Nothing
| otherwise -> Just (Set.singleton g)
MultiplyMarginal gs -> Just gs
bidMarginality :: Map.Map Good Price -> Bid bid -> Marginality
bidMarginality ps bid
| Map.null (bid ^. bid_values) = NonMarginal
| m < 0 = Unsuccessful
| m == 0, [g] <- Set.toList gs = SinglyMarginal g
| is_generalised_marginal = MultiplyMarginal gs'
| non_generalised_marginal = MultiplyMarginal gs
| otherwise = NonMarginal
where
(m, gs, m', gs') = bidSurplus ps bid
is_generalised_marginal = isGeneralised bid && case Set.size gs' of
0 -> False
1 -> m' == 0
_ -> True
non_generalised_marginal = not (isGeneralised bid) && Set.size gs >= 2
joinBidAllocations :: Ord bid => Map.Map (AVar (bid, Int)) Units -> Map.Map (AVar bid) Units
joinBidAllocations = Map.mapKeysWith (+) f
where
f :: AVar (bid, Int) -> AVar bid
f (AllocVar (bid, _) good) = AllocVar bid good
f (StepVar good i) = StepVar good i
f (ExtraAllocVar good) = ExtraAllocVar good
f (TQSSStepVar l) = TQSSStepVar l
evenlyRedistribute :: Ord bid
=> Map.Map bid (Bid bid) -> Map.Map Good Price
-> Map.Map (AVar bid) Units -> Map.Map (AVar bid) Units
evenlyRedistribute bs ps xs = Map.mapWithKey f xs
where
f (AllocVar b good)
| Just bid <- is_marginal_single b good = const (shareFor good * bidQuantityOf bid good)
f _ = id
is_marginal_single b good = case Map.lookup b bs of
Just bid | SinglyMarginal good' <- bidMarginality ps bid
, good == good' -> Just bid
_ -> Nothing
shareFor good = sum allocated / sum bid_for
where
(allocated, bid_for) = unzip [ (x, bidQuantityOf bid good)
| (AllocVar b good', x) <- Map.toList xs
, good == good'
, Just bid <- [is_marginal_single b good]
]
type BidSpec = (Units, Map.Map Good BidValue)
bidSpec :: Bid bid -> BidSpec
bidSpec bid = (bid ^. bid_quantity, bid ^. bid_values)
shareMatching :: forall bid . Ord bid
=> Auction bid
-> Map.Map bid (Map.Map Good Units) -> Map.Map bid (Map.Map Good Units)
shareMatching auction allocs =
Map.fromList [ (bid ^. bid_label, x)
| bid <- _auction_bids auction
, Just x <- [Map.lookup (bidSpec bid) m2]
]
where
m1 :: Map.Map BidSpec (Map.Map Good Units, Int)
m1 = Map.fromListWith (\ (x, y) (x', y') -> (Map.unionWith (+) x x', y + y')) $
map f (_auction_bids auction)
f bid = (bidSpec bid, (fromMaybe Map.empty (Map.lookup (bid ^. bid_label) allocs), 1))
m2 :: Map.Map BidSpec (Map.Map Good Units)
m2 = Map.map calc_share m1
calc_share (total_share, num_bids)
| num_bids > 1 = Map.map (share_between num_bids) total_share
| otherwise = total_share
share_between num_bids = floorToScale sf . (/ fromIntegral num_bids) . roundToScale sf
sf = auction ^. auction_scale
$(makeLenses ''RationingOptions)