public void Expand_Two_Markdowns_Two_Weeks() { var schedule = SmDenseSchedule.FromInteger(Convert.ToInt32("11", 2), 10, 2); var priceLadder = new SmPriceLadder { Values = new [] { 0.1M, 0.5M, 0.9M }, Type = SmPriceLadderType.Percent }; var expected = new[] { new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new decimal?[] { 0.1M, 0.5M }, Weeks = new [] { 10, 11 } }, new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new decimal?[] { 0.5M, 0.9M }, Weeks = new [] { 10, 11 } }, new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new decimal?[] { 0.1M, 0.9M }, Weeks = new [] { 10, 11 } } }; var result = schedule.Expand(priceLadder); result.ShouldAllBeEquivalentTo(expected); }
public void Expand_Two_Markdowns_On_Second_And_Fourth_Week_With_Null_First() { var schedule = SmDenseSchedule.FromInteger(Convert.ToInt32("1010", 2), 10, 4); var priceLadder = new SmPriceLadder { Values = new [] { 0.1M, 0.5M, 0.9M }, Type = SmPriceLadderType.Percent }; var expected = new[] { new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new [] { (decimal?)null, 0.1M, 0.1M, 0.5M }, Weeks = new [] { 10, 11, 12, 13 } }, new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new [] { (decimal?)null, 0.5M, 0.5M, 0.9M }, Weeks = new [] { 10, 11, 12, 13 } }, new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new [] { (decimal?)null, 0.1M, 0.1M, 0.9M }, Weeks = new [] { 10, 11, 12, 13 } } }; var result = schedule.Expand(priceLadder); result.ShouldAllBeEquivalentTo(expected); }
public void Expand_One_Markdown_One_Week() { var schedule = SmDenseSchedule.FromInteger(1, 10, 1); var priceLadder = new SmPriceLadder { Values = new [] { 0.1M, 0.5M, 0.9M }, Type = SmPriceLadderType.Percent }; var expected = new[] { new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 1, Prices = new decimal?[] { 0.1M }, Weeks = new [] { 10 } }, new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 1, Prices = new decimal?[] { 0.5M }, Weeks = new [] { 10 } }, new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 1, Prices = new decimal?[] { 0.9M }, Weeks = new [] { 10 } } }; var result = schedule.Expand(priceLadder); result.ShouldAllBeEquivalentTo(expected); }
public void Optimise_Constraints_Are_Always_Markdowns() { char[,] expected = { { '_', '_', '_', '_' }, { '_', '_', '_', '_' }, { 'x', '_', '_', '_' }, { 'x', 'x', '_', '_' }, { 'x', 'x', 'x', '_' }, { '_', 'x', 'x', 'x' }, { '_', 'x', 'x', 'x' }, { '_', '_', '_', 'x' }, { '_', '_', '_', 'x' } }; var priceLadder = new SmPriceLadder { Values = new[] { 0.1M, 0.2M, 0.3M, 0.4M, 0.5M, 0.6M, 0.7M, 0.8M, 0.9M }, Type = SmPriceLadderType.Percent }; var schedule = SmDenseSchedule.FromInteger(170, 1, 8); schedule.Constraints = new[] { SmWeekConstraint.Range(2, 0.3M, 0.5M), SmWeekConstraint.Range(6, 0.5M, 0.7M) }; var result = SmDenseSchedule.Optimise(schedule, priceLadder); Simplify(priceLadder, schedule, result) .Should() .Equal(expected, (left, right) => left == right); }
public void Optimise_Identifies_All_Potential_Week_Price_Values() { char[,] expected = { { 'x', '_', '_', '_' }, { 'x', 'x', '_', '_' }, { 'x', 'x', 'x', '_' }, { 'x', 'x', 'x', 'x' }, { 'x', 'x', 'x', 'x' }, { 'x', 'x', 'x', 'x' }, { '_', 'x', 'x', 'x' }, { '_', '_', 'x', 'x' }, { '_', '_', '_', 'x' } }; var priceLadder = new SmPriceLadder { Values = new [] { 0.1M, 0.2M, 0.3M, 0.4M, 0.5M, 0.6M, 0.7M, 0.8M, 0.9M }, Type = SmPriceLadderType.Percent }; var schedule = SmDenseSchedule.FromInteger(170, 1, 8); var result = SmDenseSchedule.Optimise(schedule, priceLadder); Simplify(priceLadder, schedule, result) .Should() .Equal(expected, (left, right) => left == right); }
private static char[,] Simplify(SmPriceLadder priceLadder, SmDenseSchedule schedule, ICollection <Tuple <int, decimal> > test) { var result = new char[priceLadder.Values.Length, schedule.MarkdownWeeks.Length]; for (var y = 0; y < priceLadder.Values.Length; y++) { for (var x = 0; x < schedule.MarkdownWeeks.Length; x++) { result[y, x] = test.Contains(Tuple.Create(schedule.MarkdownWeeks[x], priceLadder.Values[y])) ? 'x' : '_'; } } return(result); }
private static List <SmDenseSchedule> Generate(int firstWeek, int weekCount, int weeksAllowed, int weeksRequired, bool filterConsecutiveWeeks, List <SmWeekConstraint> constraints) { if (firstWeek < 0) { throw new ArgumentException("Cannot be < 0", nameof(firstWeek)); } if (weekCount < 1) { throw new ArgumentException("Cannot be < 1", nameof(weekCount)); } var range = Enumerable.Range(1, (int)Math.Pow(2, weekCount) - 1); // Any of these bits are permissible if (weeksAllowed > 0) { range = range.Where(x => x > 0 && (x & ~weeksAllowed) == 0); } // Any of these bits are must be set if (weeksRequired > 0) { range = range.Where(x => x > 0 && (x & weeksRequired) == weeksRequired); } // Filter consecutive weeks if (filterConsecutiveWeeks) { FilterConsecutiveWeeks(ref range, weekCount); } return(range .Select(i => SmDenseSchedule.FromInteger(i, firstWeek, weekCount, constraints)) .OrderBy(x => x.MarkdownCount) .ThenBy(x => x.ScheduleNumber) .ToList()); }
public void No_Markdowns() { var schedule = SmDenseSchedule.NoMarkdowns(10, 13); var priceLadder = new SmPriceLadder { Values = new [] { 0.1M, 0.5M, 0.9M }, Type = SmPriceLadderType.Percent }; var expected = new [] { new SmSchedulePricePath { LadderType = SmPriceLadderType.Percent, MarkdownCount = 0, Prices = new decimal?[] { null, null, null, null }, Weeks = new[] { 10, 11, 12, 13 } } }; var result = schedule.Expand(priceLadder); result.ShouldBeEquivalentTo(expected); }
private async Task <VmRecommendation> Revise(int clientId, SmRecommendationProductSummary product, List <VmScenarioRevision> revisions) { var constraints = revisions .Take(1) .Concat(revisions.Zip(revisions.Skip(1), (first, second) => second.Week > first.Week && second.Discount > first.Discount ? second : null)) .Where(x => x != null) .Select(x => SmWeekConstraint.Fixed(x.Week, (decimal)x.Discount)) .ToList(); if (!constraints.Any()) { throw new HttpStatusCodeException(HttpStatusCode.BadRequest, "No markdowns in the revision model data"); } var scenarioSummary = await _scenarioWebService.Get(clientId, product.ScenarioId); if (scenarioSummary == null) { throw new HttpStatusCodeException(HttpStatusCode.InternalServerError, $"Missing scenario for client id ${product.ClientId} with scenario id ${product.ScenarioId}"); } var weeks = Enumerable.Range(scenarioSummary.Scenario.ScheduleWeekMin, scenarioSummary.Scenario.ScheduleWeekMax - scenarioSummary.Scenario.ScheduleWeekMin + 1).ToList(); if (constraints.Select(x => x.Week).Except(weeks).Any()) { throw new HttpStatusCodeException(HttpStatusCode.BadRequest, "All revision weeks must be within [ScheduleWeekMin..ScheduleWeekMax]"); } var settings = MarkdownFunctionSettings.FromWebApiConfiguration(product.ModelId, 100, product.ScenarioId, _organisationDataProvider.OrganisationId.Value, _organisationDataProvider.UserId.Value, product.PartitionNumber, product.PartitionCount); var modelData = await _scenarioService.GetModelData(settings); var scenarioData = await _scenarioService.GetScenarioData(settings, settings.PartitionId); var productData = scenarioData.Item2.FirstOrDefault(x => x.ProductId == product.ProductId); if (productData == null) { throw new HttpStatusCodeException(HttpStatusCode.InternalServerError, "Missing original product by id " + product.ProductId); } var revisionValues = revisions.Select(x => (decimal)x.Discount).ToList(); var constraintValues = constraints .Select(x => x.Min) .Concat(constraints.Select(x => x.Max)) .Distinct() .Where(x => x != null) .Select(x => (decimal)x) .ToList(); if (constraintValues.Except(productData.PriceLadder.Values).Any()) { throw new HttpStatusCodeException(HttpStatusCode.BadRequest, "All revision discounts must be price ladder values"); } var modelId = product.ModelId; var revisionId = product.RevisionCount + 1; // Caculate the mask passed in var revisionMask = weeks .Select((x, i) => new { Week = x, Index = i }) .Where(item => constraints.Any(x => x.Week == item.Week)) .Aggregate(0, (current, item) => current | 1 << item.Index); var schedules = new List <SmDenseSchedule> { SmDenseSchedule.FromInteger(revisionMask, weeks.First(), weeks.Count, constraints) }; // Calculate var result = _markdownService.Calculate(scenarioSummary.Scenario, modelId, revisionId, schedules, modelData.DecayHierarchies, modelData.ElasticityHierarchies, productData, revisionValues); // Upload await _scenarioResultsService.Upload(settings, product, revisionId, result.Recommendations, _organisationDataProvider.UserId.Value); // Update state and retrieve var finalResult = await _recommendationProductService.Revise( clientId : product.ClientId, scenarioId : product.ScenarioId, productId : product.ProductId); return(VmRecommendation.Build(finalResult)); }
public SmCalcProduct Calculate( SmScenario scenario, int modelId, int revisionId, List <SmDenseSchedule> schedules, Dictionary <int, SmDecayHierarchy> decayHierarchies, Dictionary <int, SmElasticityHierarchy> elasticityHierarchies, SmProduct product, List <decimal> revisedDiscounts = null, CancellationToken cancellationToken = default(CancellationToken)) { // Calculate the current markdown depth and ladder depth var depth = GetDiscountLadderDepth(product); var result = new SmCalcProduct(scenario, modelId, product, schedules, depth); // Return OK if there are no schedules for this product if (!schedules.Any()) { _logger.Warning("No schedules for {ProductId}", product.ProductId); return(result.Ok(ProductState.NoSchedules)); } // Return Fatal recommendation for bad CSPs if (product.CurrentSellingPrice <= 0) { _logger.Warning("Product {ProductId} CSP is <= 0", product.ProductId); return(result.Fatal(ProductState.InvalidCsp)); } // Return Fatal if we can't resolve the decay hierarchy if (!decayHierarchies.TryGetValue(product.HierarchyId, out SmDecayHierarchy decayHierarchy)) { _logger.Warning("Can't find Decay Hierarchy by HierarchyId {HierarchyId} for Product {ProductId}", product.HierarchyId, product.ProductId); return(result.Fatal(ProductState.InvalidDecayHierarchy)); } // Return Fatal if we can't resolve the elasticity hierarchy if (!elasticityHierarchies.TryGetValue(product.HierarchyId, out SmElasticityHierarchy elasticityHierarchy)) { _logger.Warning("Can't find Elasticity Hierarchy by HierarchyId {HierarchyId} for Product {ProductId}", product.HierarchyId, product.ProductId); return(result.Fatal(ProductState.InvalidElasticityHierarchy)); } var recommendations = new List <SmCalcRecommendation>(); foreach (var schedule in schedules) { // Apply the product mask to this schedule and skip of not aligned if (product.ProductScheduleMask != null && (schedule.ScheduleNumber & product.ProductScheduleMask.Value) != 0) { result.ScheduleProductMaskFilterCount++; continue; } // Skip products where the max markdown is exceeded if (product.ProductMaxMarkdown != null && product.ProductMaxMarkdown.Value >= schedule.MarkdownCount) { result.ScheduleMaxMarkdownFilterCount++; continue; } if (product.ProductHasExceededFlowlineThreshold == 1) { result.ScheduleExceededFlowlineThresholdFilterCount++; continue; } var crossProduct = GetCrossProduct(schedule, product.PriceLadder); result.ScheduleCrossProductCount = crossProduct.Length; foreach (var path in crossProduct) { var prices = path.Prices.Select(x => x ?? 0).ToArray(); var recommendation = new SmCalcRecommendation(scenario, schedule, prices, revisionId); var calculateResult = CalculatePricePath( product: ref result, recommendation: ref recommendation, scenario: scenario, modelId: modelId, revisionId: revisionId, decayHierarchy: decayHierarchy, elasticityHierarchy: elasticityHierarchy, priceLadder: product.PriceLadder, scheduleId: schedule.ScheduleNumber, schedulePricePath: path); if (calculateResult) { // Get top 10 by Total Revenue recommendations.InsertAfter(recommendation, 10, x => recommendation.TotalRevenue > x.TotalRevenue); } cancellationToken.ThrowIfCancellationRequested(); } } // For initial runs, calculate CSP (where revision id = 0) if (revisionId == 0) { var noChangeSchedule = SmDenseSchedule.NoMarkdowns(scenario.ScheduleWeekMin, scenario.ScheduleWeekMax); var cspPricePath = SmSchedulePricePath.Build(noChangeSchedule.WeekMin, noChangeSchedule.WeekMax, noChangeSchedule.MarkdownWeeks, product.PriceLadder.Type, product.PriceLadder.Values); var prices = cspPricePath.Prices.Select(x => x ?? 0).ToArray(); var cspRecommendation = new SmCalcRecommendation(scenario, noChangeSchedule, prices, revisionId, isCsp: true); var cspResult = CalculatePricePath( product: ref result, recommendation: ref cspRecommendation, scenario: scenario, modelId: modelId, revisionId: revisionId, decayHierarchy: decayHierarchy, elasticityHierarchy: elasticityHierarchy, priceLadder: product.PriceLadder, scheduleId: noChangeSchedule.ScheduleNumber, schedulePricePath: cspPricePath); if (cspResult) { recommendations.InsertAfter(cspRecommendation, recommendations.Count + 1, x => cspRecommendation.TotalRevenue > x.TotalRevenue, true); } result.ScheduleCount++; } if (!recommendations.Any()) { _logger.Warning("No recommendations made for {ProductId}", product.ProductId); return(result.Ok()); } // Set rank, store and set stats var results = new List <SmCalcRecommendation>(); foreach (var ordered in recommendations.Select((x, i) => new { recomendation = x, index = i })) { var recommendation = ordered.recomendation; recommendation.Rank = recommendations.Count - ordered.index; results.Add(recommendation); } ExpandProjectionWeeks(ref result, ref results, scenario, revisedDiscounts); return(result.Ok(results)); }
private SmSchedulePricePath[] GetCrossProduct(SmDenseSchedule schedule, SmProductPriceLadder ladder) { var constraintId = schedule.Constraints.GetHashCode(); var key = new { schedule.MarkdownCount, Mask = schedule.ScheduleNumber, ladder.PriceLadderId, constraintId }; if (!_ladderPathCache.TryGetValue(key, out SmSchedulePricePath[] result))