From 42a958ecb25936aed0590a6dcbfb0d0e22ebe918 Mon Sep 17 00:00:00 2001 From: iMoHax Date: Sat, 16 May 2015 17:23:09 +0300 Subject: [PATCH] implement scorer --- .../ru/trader/analysis/FilteredMarket.java | 10 +- .../main/java/ru/trader/analysis/Scorer.java | 100 +- .../src/main/java/ru/trader/core/Profile.java | 31 + core/src/main/java/ru/trader/core/Ship.java | 176 +- .../java/ru/trader/analysis/ScorerTest.java | 116 + core/src/test/resources/test3.xml | 10135 +++++++++++++++- 6 files changed, 10528 insertions(+), 40 deletions(-) create mode 100644 core/src/test/java/ru/trader/analysis/ScorerTest.java diff --git a/core/src/main/java/ru/trader/analysis/FilteredMarket.java b/core/src/main/java/ru/trader/analysis/FilteredMarket.java index d2e22eb..0803b9d 100644 --- a/core/src/main/java/ru/trader/analysis/FilteredMarket.java +++ b/core/src/main/java/ru/trader/analysis/FilteredMarket.java @@ -3,6 +3,7 @@ package ru.trader.analysis; import ru.trader.core.*; import java.util.Collection; +import java.util.NavigableSet; import java.util.stream.Stream; public class FilteredMarket { @@ -37,8 +38,13 @@ public class FilteredMarket { } public Stream getOffers(OFFER_TYPE offerType, Item item){ - return market.getStat(offerType, item).getOffers().stream() - .filter(o -> !filter.isFiltered(o.getVendor(), true)); + NavigableSet offers = market.getStat(offerType, item).getOffers(); + Stream res; + if (offerType.getOrder() > 0) + res = offers.stream(); + else + res = offers.descendingSet().stream(); + return res.filter(o -> !filter.isFiltered(o.getVendor(), true)); } diff --git a/core/src/main/java/ru/trader/analysis/Scorer.java b/core/src/main/java/ru/trader/analysis/Scorer.java index 6759232..336b717 100644 --- a/core/src/main/java/ru/trader/analysis/Scorer.java +++ b/core/src/main/java/ru/trader/analysis/Scorer.java @@ -1,8 +1,10 @@ package ru.trader.analysis; +import org.jetbrains.annotations.NotNull; import ru.trader.core.*; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; public class Scorer { @@ -11,9 +13,11 @@ public class Scorer { private final FilteredMarket market; private final Profile profile; - private int ordersCount = 5; + private final double avgProfit; + private final double avgDistance; - private double avgProfit; + private int ordersCount = 5; + private double distanceRate = 1; public Scorer(FilteredMarket market, Profile profile) { this.market = market; @@ -22,6 +26,7 @@ public class Scorer { buyOffers = new HashMap<>(100, 0.9f); market.getItems().parallelStream().forEach(this::fillOffers); avgProfit = computeAvgProfit(); + avgDistance = computeAvgDistance(); } private void fillOffers(Item item){ @@ -35,22 +40,33 @@ public class Scorer { } } - public void setOrdersCount(int ordersCount) { - this.ordersCount = ordersCount; - } - private double computeAvgProfit(){ OptionalDouble avg = sellOffers.values().stream() .flatMap(this::mapToOrder) - .mapToDouble(Order::getProfit) + .mapToDouble(o -> o.getProfit() / profile.getShip().getCargo()) .average(); return avg.orElse(0); } + private double computeAvgDistance(){ + OptionalDouble res = market.getVendors().mapToDouble(Vendor::getDistance).average(); + return res.orElse(0); + } + + public void setOrdersCount(int ordersCount) { + this.ordersCount = ordersCount; + } + + public void setDistanceRate(double distanceRate) { + this.distanceRate = distanceRate; + } + + public double getAvgProfit() { + return avgProfit; + } + public Score getScore(Vendor vendor){ - Stream sellOrders = vendor.getAllSellOffers().stream().flatMap(this::mapToOrder); - Stream buyOrders = vendor.getAllBuyOffers().stream().flatMap(this::mapToOrder); - return new Score(sellOrders, buyOrders); + return new Score(vendor); } private Stream mapToOrder(Offer offer) { @@ -69,33 +85,59 @@ public class Scorer { return Stream.of(order); } - public class Score { - private double sellProfit; - private double buyProfit; + public class Score implements Comparable { + private final Vendor vendor; + private final DoubleSummaryStatistics sellStat; + private final DoubleSummaryStatistics buyStat; private double score; - public Score(Stream sell, Stream buy) { - sellProfit = computeProfits(sell); - buyProfit = computeProfits(buy); + public Score(Vendor vendor) { + this.vendor = vendor; + Stream sell = vendor.getAllSellOffers().stream().flatMap(Scorer.this::mapToOrder); + Stream buy = vendor.getAllBuyOffers().stream().flatMap(Scorer.this::mapToOrder); - long count = sell.limit(ordersCount).count(); - computeScore(count); + sellStat = computeProfits(sell); + buyStat = computeProfits(buy); + + computeScore(); } - private double computeProfits(Stream orders) { - OptionalDouble profit = orders.sorted(Comparator.reverseOrder()) - .limit(ordersCount) - .mapToDouble(Order::getProfit) - .average(); - return profit.orElse(0); + public double getSellProfit() { + return sellStat.getAverage(); } - private void computeScore(long sellOrdersCount){ - score = (sellProfit + buyProfit)/2; - if (sellOrdersCount < ordersCount){ - score =- Math.abs(avgProfit-sellProfit) * (ordersCount - sellOrdersCount) / score; - } + public double getBuyProfit() { + return buyStat.getAverage(); + } + public double getScore() { + return score; + } + + private DoubleSummaryStatistics computeProfits(Stream orders) { + return orders.sorted(Comparator.reverseOrder()) + .limit(ordersCount) + .collect(Collectors.summarizingDouble(o -> o.getProfit() / profile.getShip().getCargo())); + } + + private void computeScore(){ + score = (getSellProfit() + getBuyProfit())/2; + score -= distanceRate * avgProfit * (vendor.getDistance() - avgDistance) / avgDistance; + } + + @Override + public String toString() { + return "Score{" + + "vendor=" + vendor.getPlace()+"("+vendor+")"+ + ", sellStat=" + sellStat + + ", buyStat=" + buyStat + + ", score=" + score + + '}'; + } + + @Override + public int compareTo(@NotNull Score other) { + return Double.compare(score, other.score); } } diff --git a/core/src/main/java/ru/trader/core/Profile.java b/core/src/main/java/ru/trader/core/Profile.java index c0b81c3..cb37276 100644 --- a/core/src/main/java/ru/trader/core/Profile.java +++ b/core/src/main/java/ru/trader/core/Profile.java @@ -3,13 +3,44 @@ package ru.trader.core; public class Profile { private double balance; + private int jumps; private Ship ship; + private boolean refill; + + public Profile(Ship ship) { + this.ship = ship; + refill = true; + } public double getBalance() { return balance; } + public void setBalance(double balance) { + this.balance = balance; + } + + public int getJumps() { + return jumps; + } + + public void setJumps(int jumps) { + this.jumps = jumps; + } + public Ship getShip() { return ship; } + + public void setShip(Ship ship) { + this.ship = ship; + } + + public boolean withRefill() { + return refill; + } + + public void setRefill(boolean refill) { + this.refill = refill; + } } diff --git a/core/src/main/java/ru/trader/core/Ship.java b/core/src/main/java/ru/trader/core/Ship.java index 6fb906d..e2247e8 100644 --- a/core/src/main/java/ru/trader/core/Ship.java +++ b/core/src/main/java/ru/trader/core/Ship.java @@ -1,17 +1,26 @@ package ru.trader.core; public class Ship { - private int cargo; - private double engine; + private Engine engine; + private double tank; + private double mass; - public Ship(int cargo, double engine) { - this.cargo = cargo; - this.engine = engine; + public Ship() { + //Default sidewinder + this.mass = 44.9; + this.cargo = 4; + this.tank = 2; + this.engine = new Engine(2, 'E'); } public static Ship copyOf(Ship other){ - return new Ship(other.cargo, other.engine); + Ship copy = new Ship(); + copy.mass = other.mass; + copy.cargo = other.cargo; + copy.tank = other.tank; + copy.engine = other.getEngine(); + return copy; } public int getCargo() { @@ -22,12 +31,163 @@ public class Ship { this.cargo = cargo; } - public double getEngine() { + public Engine getEngine() { return engine; } - public void setEngine(double engine) { + public void setEngine(Engine engine) { this.engine = engine; } + public double getTank() { + return tank; + } + + public void setTank(double tank) { + this.tank = tank; + } + + public double getMass() { + return mass; + } + + public void setMass(double mass) { + this.mass = mass; + } + + public double getLadenMass(){ + return mass+tank+cargo; + } + + public double getLadenMass(double fuel){ + return mass+fuel+cargo; + } + + //Laden fuel cost + public double getFuelCost(double distance){ + return engine.getFuelCost(distance, getLadenMass()); + } + + public double getFuelCost(double fuel, double distance){ + return engine.getFuelCost(distance, getLadenMass(fuel)); + } + + //Jump range with full fuel tank + public double getJumpRange(){ + return getJumpRange(tank); + } + + //Laden jump range + public double getJumpRange(double fuel){ + return engine.getJumpRange(fuel, getLadenMass(fuel)); + } + + public double getFullTankJumpRange(){ + double fuel = tank; + double range = 0; + while (fuel > 0){ + double distance = engine.getJumpRange(fuel, getLadenMass(fuel)); + range += distance; + fuel -= engine.getFuelCost(distance, getLadenMass(fuel)); + } + return range; + } + + + @Override + public String toString() { + return "Ship{" + + "cargo=" + cargo + + ", engine=" + engine + + ", tank=" + tank + + ", mass=" + mass + + ", maxDist=" + getJumpRange() + + ", fullTankDist=" + getFullTankJumpRange() + + '}'; + } + + //FSD multiplier by FSD Rating A,B,C ... etc * 0.001 + //http://elite-dangerous.wikia.com/wiki/Frame_Shift_Drive + private final static double[] FSD_MULT = {0.012,0.010,0.008,0.010,0.011}; + //FSD power multiplier by FSD Class 1,2,3 ... etc + private final static double[] FSD_POWER_MULT = {0,0,2.00,2.15,2.30,2.45,2.60,2.75,2.90}; + //FSD Optimal Mass [class][rating] + private final static double[][] FSD_OPT_MASS = { + {}, + {}, + {90.0, 75.0, 60.0, 54.0, 48.0}, + {150.0, 125.0, 100.0, 90.0, 80.0}, + {525.0, 438.0, 350.0, 315.0, 280.0}, + {1,050.0, 875.0, 700.0, 630.0, 560.0}, + {1,800.0, 1,500.0, 1,200.0, 1,080.0, 960.0} + }; + //FSD Max fuel per jump [class][rating] + private final static double[][] FSD_MAX_FUEL= { + {}, + {}, + {0.90, 0.80, 0.60, 0.60, 0.60}, + {1.80, 1.50, 1.20, 1.20, 1.20}, + {3.00, 2.50, 2.00, 2.00, 2.00}, + {5.00, 4.10, 3.30, 3.30, 3.30}, + {8.00, 6.60, 5.30, 5.30, 5.30} + }; + + private class Engine { + private int rating; + private int clazz; + + private Engine(int clazz, char rating) { + setRating(rating); + this.clazz = clazz; + } + + public char getRating() { + return (char)(rating + 'A'); + } + + public void setRating(char rating) { + this.rating = rating - 'A'; + } + + public int getClazz() { + return clazz; + } + + public void setClazz(int clazz) { + this.clazz = clazz; + } + + public double getOptMass() { + return FSD_OPT_MASS[clazz][rating]; + } + + public double getMaxFuel() { + return FSD_MAX_FUEL[clazz][rating]; + } + + public double getMultiplier(){ + return FSD_MULT[rating]; + } + + public double getPowMultiplier(){ + return FSD_POWER_MULT[clazz]; + } + + //https://forums.frontier.co.uk/showthread.php?p=643461#post643461 + //Fuel Cost = Coefficient * (Distance * (Mass / Optimised Mass))^Power + public double getFuelCost(double distance, double mass){ + return getMultiplier() * Math.pow(distance * (mass / getOptMass()), getPowMultiplier()); + } + + public double getJumpRange(double fuel, double mass){ + return Math.pow(Math.min(fuel, getMaxFuel())/getMultiplier(), 1/getPowMultiplier())*getOptMass()/mass; + } + + @Override + public String toString() { + return ""+clazz+getRating()+ + " {optMass="+getOptMass()+ + ", fuelPJ="+getMaxFuel()+"}"; + } + } } diff --git a/core/src/test/java/ru/trader/analysis/ScorerTest.java b/core/src/test/java/ru/trader/analysis/ScorerTest.java new file mode 100644 index 0000000..4562106 --- /dev/null +++ b/core/src/test/java/ru/trader/analysis/ScorerTest.java @@ -0,0 +1,116 @@ +package ru.trader.analysis; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.trader.core.*; +import ru.trader.store.simple.Store; + +import java.io.InputStream; + + +public class ScorerTest extends Assert { + private final static Logger LOG = LoggerFactory.getLogger(ScorerTest.class); + + private Market world; + private FilteredMarket fWorld; + + private Place breksta; + private Place bhadaba; + private Place lhs1541; + private Place itza; + + @Before + public void setUp() throws Exception { + InputStream is = getClass().getResourceAsStream("/test3.xml"); + world = Store.loadFromFile(is); + breksta = world.get("Breksta"); + bhadaba = world.get("Bhadaba"); + lhs1541 = world.get("LHS 1541"); + itza = world.get("Itza"); + + MarketFilter filter = new MarketFilter(); + fWorld = new FilteredMarket(world, filter); + } + + @Test + public void testScore() throws Exception { + Vendor grantTerminal = breksta.get("Grant Terminal"); + Vendor perezMarket = breksta.get("Perez market"); + Vendor kandelRing = bhadaba.get("Kandel Ring"); + Vendor robertsHub = bhadaba.get("Roberts Hub"); + Vendor cabreraDock = lhs1541.get("Cabrera Dock"); + Vendor hallerPort = lhs1541.get("Haller Port"); + Vendor luikenPort = itza.get("Luiken Port"); + Ship ship = new Ship(); + ship.setCargo(100); + Profile profile = new Profile(ship); + LOG.info("Start score test, balance 10000000"); + profile.setBalance(10000000); + Scorer scorer = new Scorer(fWorld, profile); + if (LOG.isDebugEnabled()){ + fWorld.getVendors().map(scorer::getScore).sorted().forEach(s -> LOG.debug("{}", s)); + } + + Scorer.Score gtScore = scorer.getScore(grantTerminal); + Scorer.Score pmScore = scorer.getScore(perezMarket); + assertTrue(gtScore.getSellProfit() < pmScore.getSellProfit()); + assertTrue(gtScore.getBuyProfit() > pmScore.getBuyProfit()); + assertTrue(gtScore.getScore() > pmScore.getScore()); + + Scorer.Score krScore = scorer.getScore(kandelRing); + Scorer.Score rhScore = scorer.getScore(robertsHub); + assertTrue(krScore.getScore() < pmScore.getScore()); + assertTrue(krScore.getScore() < rhScore.getScore()); + assertTrue(krScore.getScore() < gtScore.getScore()); + + Scorer.Score cdScore = scorer.getScore(cabreraDock); + Scorer.Score hpScore = scorer.getScore(hallerPort); + Scorer.Score lpScore = scorer.getScore(luikenPort); + assertTrue(hpScore.getScore() > pmScore.getScore()); + assertTrue(hpScore.getScore() > krScore.getScore()); + assertTrue(hpScore.getScore() > gtScore.getScore()); + assertTrue(hpScore.getScore() > lpScore.getScore()); + assertTrue(hpScore.getScore() > cdScore.getScore()); + assertTrue(cdScore.getScore() > pmScore.getScore()); + assertTrue(cdScore.getScore() > krScore.getScore()); + assertTrue(cdScore.getScore() > gtScore.getScore()); + assertTrue(cdScore.getScore() > lpScore.getScore()); + + LOG.info("Start score test, balance 50000"); + profile.setBalance(50000); + Scorer scorer2 = new Scorer(fWorld, profile); + if (LOG.isDebugEnabled()){ + fWorld.getVendors().map(scorer2::getScore).sorted().forEach(s -> LOG.debug("{}", s)); + } + + gtScore = scorer2.getScore(grantTerminal); + pmScore = scorer2.getScore(perezMarket); + assertTrue(gtScore.getSellProfit() > pmScore.getSellProfit()); + assertTrue(gtScore.getBuyProfit() > pmScore.getBuyProfit()); + assertTrue(gtScore.getScore() > pmScore.getScore()); + + krScore = scorer2.getScore(kandelRing); + rhScore = scorer2.getScore(robertsHub); + cdScore = scorer2.getScore(cabreraDock); + hpScore = scorer2.getScore(hallerPort); + lpScore = scorer2.getScore(luikenPort); + + assertTrue(lpScore.getScore() > pmScore.getScore()); + assertTrue(lpScore.getScore() > gtScore.getScore()); + assertTrue(lpScore.getScore() > krScore.getScore()); + assertTrue(lpScore.getScore() > rhScore.getScore()); + assertTrue(lpScore.getScore() > cdScore.getScore()); + assertTrue(lpScore.getScore() > hpScore.getScore()); + + } + + @After + public void tearDown() throws Exception { + world = null; + fWorld = null; + } +} diff --git a/core/src/test/resources/test3.xml b/core/src/test/resources/test3.xml index d791e7c..6bdbba3 100644 --- a/core/src/test/resources/test3.xml +++ b/core/src/test/resources/test3.xml @@ -1 +1,10134 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file