diff --git a/.stylecop/StyleCop.props b/.stylecop/StyleCop.props index 6e01fabb9..294439ffd 100644 --- a/.stylecop/StyleCop.props +++ b/.stylecop/StyleCop.props @@ -4,7 +4,7 @@ False - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..eaa976bc7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + // "program": "${workspaceFolder}/MetaOptimize.Test/bin/Debug/net8.0/MetaOptimize.Test.dll", + "program": "${workspaceFolder}/MetaOptimize.Cli/bin/Debug/net8.0/MetaOptimize.Cli.exe", + "args": [], + "cwd": "${workspaceFolder}/MetaOptimize.Cli", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..d0c8d23d3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "dotnet.defaultSolution": "MetaOptimize.sln", + "debugpy.debugJustMyCode": false, + "jupyter.debugJustMyCode": false, + "csharp.debug.justMyCode": false +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..a14aa16e2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/MetaOptimize.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/MetaOptimize.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/MetaOptimize.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..193280892 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..5079c9bf0 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,42 @@ + + + + strict + disable + 9999 + preview + enable + net8.0 + True + true + + + x64 + + Microsoft + © Microsoft Corporation. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MetaOptimize.Cli/BPRunner.cs b/MetaOptimize.Cli/BPRunner.cs new file mode 100644 index 000000000..b723626bf --- /dev/null +++ b/MetaOptimize.Cli/BPRunner.cs @@ -0,0 +1,135 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace MetaOptimize.Cli +{ + using System.Diagnostics; + using Gurobi; + using ZenLib; + using ZenLib.ModelChecking; + + /// + /// Runner for Vector Bin Packing adversarial optimization. + /// Finds item sizes that maximize the gap between optimal packing and First-Fit heuristics. + /// + /// + /// Flow: Configure bins → Create encoders → Run adversarial optimization → Display gap. + /// + /// Compares optimal bin packing solution against First-Fit variants (FF, FFDSum, FFDProd, FFDDiv). + /// The adversarial generator finds item sizes where the heuristic uses significantly more bins + /// than the optimal solution. + /// + /// Supports both Gurobi (MIP) and Zen (SMT) solvers via generic implementation. + /// + public static class BPRunner + { + /// + /// Runs Bin Packing adversarial optimization. + /// Dispatches to the appropriate solver-specific implementation. + /// + /// Command-line options containing bin packing parameters. + /// Thrown when an unsupported solver is specified. + public static void Run(CliOptions opts) + { + switch (opts.SolverChoice) + { + case SolverChoice.OrTools: + RunBinPacking(new ORToolsSolver(), opts); + break; + case SolverChoice.Zen: + RunBinPacking(new SolverZen(), opts); + break; + case SolverChoice.Gurobi: + RunBinPacking( + new GurobiSOS(timeout: opts.Timeout, verbose: Convert.ToInt32(opts.Verbose)), + opts); + break; + default: + throw new Exception($"Unsupported solver: {opts.SolverChoice}. Valid options: OrTools, Gurobi, Zen"); + } + } + + /// + /// Generic implementation of bin packing adversarial optimization. + /// + /// Solver variable type (GRBVar or Zen). + /// Solver solution type (GRBModel or ZenSolution). + /// The solver instance to use. + /// Command-line options containing bin packing parameters. + /// + /// Creates three components: + /// 1. VBPOptimalEncoder: Encodes the optimal bin packing problem + /// 2. FFDItemCentricEncoder: Encodes the First-Fit heuristic behavior + /// 3. VBPAdversarialInputGenerator: Finds item sizes maximizing the gap + /// + /// The optimization finds item sizes such that: + /// - Optimal solution uses exactly opts.OptimalBins bins + /// - FFD heuristic uses as many bins as possible + /// - Gap = FFD bins - Optimal bins is maximized. + /// + private static void RunBinPacking(ISolver solver, CliOptions opts) + { + Console.WriteLine($"Bins: {opts.NumBins}, Items: {opts.NumDemands}, Dimensions: {opts.NumDimensions}"); + Console.WriteLine($"Target optimal bins: {opts.OptimalBins}"); + Console.WriteLine($"FF Method: {opts.FFMethod}"); + + // Parse bin capacities from comma-separated string + var binCapacities = opts.BinCapacity.Split(',').Select(double.Parse).ToList(); + + // Pad capacities to match number of dimensions + while (binCapacities.Count < opts.NumDimensions) + { + binCapacities.Add(1.00001); + } + + // Create bin configuration + var bins = new Bins(opts.NumBins, binCapacities); + + // Create optimal encoder - finds minimum bins needed + var optimalEncoder = new VBPOptimalEncoder( + solver, opts.NumDemands, opts.NumDimensions, BreakSymmetry: opts.BreakSymmetry); + + // Create FFD encoder - simulates First-Fit heuristic behavior + var ffdEncoder = new FFDItemCentricEncoder( + solver, opts.NumDemands, opts.NumDimensions); + + // Create adversarial generator - finds worst-case item sizes + var adversarialGenerator = new VBPAdversarialInputGenerator( + bins, opts.NumDemands, opts.NumDimensions); + + var timer = Stopwatch.StartNew(); + + // Run bilevel optimization to find adversarial inputs + List> demandList = null; + var (optimalSolution, ffdSolution) = adversarialGenerator.MaximizeOptimalityGapFFD( + optimalEncoder, ffdEncoder, + opts.OptimalBins, + ffdMethod: opts.FFMethod, + itemList: demandList, + verbose: opts.Verbose); + + timer.Stop(); + + // Display results + Console.WriteLine("\n" + new string('=', 60)); + Console.WriteLine("RESULTS:"); + Console.WriteLine($"Optimal bins used: {optimalSolution.TotalNumBinsUsed}"); + Console.WriteLine($"{opts.FFMethod} bins used: {ffdSolution.TotalNumBinsUsed}"); + Console.WriteLine($"Gap: {ffdSolution.TotalNumBinsUsed - optimalSolution.TotalNumBinsUsed}"); + Console.WriteLine($"Time: {timer.ElapsedMilliseconds}ms"); + Console.WriteLine(new string('=', 60)); + + // Verbose output: full solution details as JSON + if (opts.Verbose) + { + Console.WriteLine("\nOptimal Solution:"); + Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject( + optimalSolution, Newtonsoft.Json.Formatting.Indented)); + Console.WriteLine("\nHeuristic Solution:"); + Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject( + ffdSolution, Newtonsoft.Json.Formatting.Indented)); + } + } + } +} \ No newline at end of file diff --git a/MetaOptimize.Cli/CliOptions.cs b/MetaOptimize.Cli/CliOptions.cs index 326c2f1ff..ba1417ec8 100644 --- a/MetaOptimize.Cli/CliOptions.cs +++ b/MetaOptimize.Cli/CliOptions.cs @@ -7,408 +7,805 @@ namespace MetaOptimize.Cli using CommandLine; /// - /// The CLI command line arguments. + /// Command-line arguments for all MetaOptimize problem types. /// + /// + /// Supports four problem types: + /// - TrafficEngineering: Find worst-case demand patterns for routing heuristics + /// - BinPacking: Find adversarial item sizes that maximize FFD vs optimal gap + /// - PIFO: Find packet sequences that maximize scheduling inversions + /// - FailureAnalysis: Analyze network resilience under link failures + /// + /// Parameters are organized by: + /// - Common: Shared across all problem types + /// - Traffic Engineering: TE-specific options (largest section, most mature) + /// - Bin Packing: VBP-specific options + /// - PIFO: Packet scheduling options + /// - Failure Analysis: Network resilience options. + /// public class CliOptions { /// - /// The instance of the command line arguments. + /// Singleton instance of the parsed command-line arguments. + /// Set by Program.Main() after parsing. /// public static CliOptions Instance { get; set; } + #region Common Options + /// - /// The topology file path. + /// The problem type to solve. + /// Determines which runner is invoked and which parameters are relevant. /// - [Option('f', "file", Required = true, HelpText = "The location of the topology file.")] - public string TopologyFile { get; set; } + [Option("problemType", Default = ProblemType.BinPacking, HelpText = "Problem type: TrafficEngineering, BinPacking, PIFO, FailureAnalysis")] + public ProblemType ProblemType { get; set; } /// - /// The heuristic encoder to use. + /// The solver to use for optimization. + /// Gurobi uses MIP (Mixed Integer Programming), Zen uses SMT (Satisfiability Modulo Theories). /// - /// TODO: Is this specific to only the TE use-case? if so, is there a way to make a separate data structure for each heuristic example to make the code cleaner? - [Option('h', "heuristic", Required = true, HelpText = "The heuristic encoder to use (Pop | DemandPinning | ExpectedPop).")] - public Heuristic Heuristic { get; set; } + [Option('c', "solver", Default = SolverChoice.OrTools, HelpText = "The solver to use (Gurobi | Zen)")] + public SolverChoice SolverChoice { get; set; } /// - /// The solver we want to use. + /// Timeout for the solver in seconds. + /// Solver terminates and returns best solution found after this time. /// - [Option('c', "solver", Required = true, HelpText = "The solver that we want to use (Gurobi | Zen)")] - public SolverChoice SolverChoice { get; set; } + [Option('o', "timeout", Default = double.PositiveInfinity, HelpText = "Solver timeout in seconds (default: no timeout)")] + public double Timeout { get; set; } /// - /// inner encoding (KKT or PrimalDual). + /// Enable verbose output for debugging. + /// Shows detailed solver progress, intermediate solutions, and timing information. /// - [Option('e', "innerencoding", Default = InnerRewriteMethodChoice.KKT, HelpText = "Method to use for inner encoding.")] - public InnerRewriteMethodChoice InnerEncoding { get; set; } + [Option('v', "verbose", Default = false, HelpText = "Enable verbose output with detailed logs")] + public bool Verbose { get; set; } /// - /// adversarial generator (Encoding or Benders). + /// Enable debug output. + /// Prints additional debugging messages to standard output. /// - /// TODO: the discription of this input is not clear, explain it more. - [Option('e', "adversarialgen", Default = AdversarialGenMethodChoice.Encoding, HelpText = "Method to use for adversarial generator. The benders decomposition approach currently does not work.")] - public AdversarialGenMethodChoice AdversarialGen { get; set; } + [Option('d', "debug", Default = false, HelpText = "Prints debugging messages to standard output")] + public bool Debug { get; set; } + + #endregion + + #region Traffic Engineering Options + + /// + /// The topology file path. + /// JSON file containing network nodes and links with capacities. + /// + /// + /// Expected format: + /// { + /// "nodes": [{"id": "a"}, {"id": "b"}, ...], + /// "links": [{"source": "a", "target": "b", "capacity": 10}, ...] + /// }. + /// + [Option('f', "topologyFile", Default = "..\\Topologies\\simple.json", HelpText = "The location of the topology file (JSON format)")] + public string TopologyFile { get; set; } + + /// + /// The heuristic encoder to use. + /// + /// + /// Available heuristics: + /// - Pop: Partition-based routing with random demand partitioning + /// - DemandPinning: Threshold-based path selection (pin large demands to shortest path) + /// - ExpectedPop: Average performance across multiple partition samples + /// - PopDp: Combined Pop and DemandPinning + /// - ExpectedPopDp: Average gap of running Pop and DemandPinning in parallel + /// - ParallelPop: Multiple Pop instances in parallel + /// - ParallelPopDp: Multiple Pop instances with DemandPinning + /// - ModifiedDp: DemandPinning with upper bound on pinned path lengths. + /// + /// TODO: Is this specific to only the TE use-case? If so, is there a way to make a separate + /// data structure for each heuristic example to make the code cleaner? + [Option('h', "heuristic", Default = Heuristic.Pop, HelpText = "The heuristic encoder to use (Pop | DemandPinning | ExpectedPop | PopDp | ModifiedDp)")] + public Heuristic Heuristic { get; set; } /// - /// demand list (only applies for PrimalDual). + /// Inner encoding method for bilevel optimization. + /// KKT uses Karush-Kuhn-Tucker conditions, PrimalDual uses primal-dual reformulation. /// - /// TODO: this terminology is too TE specific. Can you make it more general to apply to our other heuristics too? Also would be good to expand the comment. - [Option('d', "demandlist", Default = "0", HelpText = "quantized list of demands (only applies to PrimalDual -- should separate value with ',' no space).")] - public String DemandList { get; set; } + /// + /// KKT: Converts inner optimization to constraints using optimality conditions. + /// PrimalDual: Uses strong duality to reformulate inner problem. + /// PrimalDual requires demand quantization (see DemandList). + /// + [Option('e', "innerencoding", Default = InnerRewriteMethodChoice.KKT, HelpText = "Method for inner encoding (KKT | PrimalDual)")] + public InnerRewriteMethodChoice InnerEncoding { get; set; } /// - /// whether to simplify the final solution or not. + /// Adversarial generator method. /// - /// TODO: this is too vague. Explain what it means to simplify the solution. - [Option('s', "simplify", Default = false, HelpText = "Whether to simplify the final solution or not")] - public bool Simplify { get; set; } + /// TODO: The description of this input is not clear, explain it more. + /// Encoding uses direct bilevel formulation, Benders uses decomposition (currently broken). + [Option("adversarialgen", Default = AdversarialGenMethodChoice.Encoding, HelpText = "Adversarial generator method (Encoding | Benders). Benders decomposition currently does not work.")] + public AdversarialGenMethodChoice AdversarialGen { get; set; } /// - /// Timeout for gurobi solver. + /// Quantized list of demand values for PrimalDual encoding. + /// Comma-separated values without spaces. /// - [Option('o', "timeout", Default = double.PositiveInfinity, HelpText = "gurobi solver terminates after the specified time")] - public double Timeout { get; set; } + /// + /// Example: "0,5,10,15,20" allows demands to take only these discrete values. + /// Required when using PrimalDual inner encoding for tractable optimization. + /// More values = finer granularity but slower optimization. + /// + /// TODO: This terminology is too TE specific. Can you make it more general to apply to + /// our other heuristics too? Also would be good to expand the comment. + [Option("demandlist", Default = "0", HelpText = "Quantized demand values (comma-separated, no spaces). Only applies to PrimalDual encoding.")] + public string DemandList { get; set; } + + /// + /// Whether to simplify the final solution. + /// + /// TODO: This is too vague. Explain what it means to simplify the solution. + /// Simplification may remove redundant flow allocations or round near-zero values. + [Option('s', "simplify", Default = false, HelpText = "Whether to simplify the final solution")] + public bool Simplify { get; set; } /// - /// terminates if no improvement after specified time. + /// Terminate if no improvement in best objective after specified time. + /// Only applies to MIP (Mixed Integer Programming) problems. + /// Value of -1 disables this termination condition. /// - [Option('x', "timetoterminate", Default = -1, HelpText = "gurobi solver terminates if no improvement in best objective after the specified time (only applies to MIP)")] + [Option('x', "timetoterminate", Default = -1.0, HelpText = "Terminate if no improvement after specified seconds (MIP only, -1 to disable)")] public double TimeToTerminateIfNoImprovement { get; set; } /// - /// The number of pop slices to use. + /// The number of partitions (slices) for Pop heuristic. + /// Demands are randomly assigned to partitions, each optimized separately. /// - /// TODO: again this is specific to a particular heuristic, is there a way to separate inputs that are heuristic specific from those that are general? - /// One way may be to take a json as input that contains the inputs that are specific to the particular heuristic. - [Option('s', "slices", Default = 2, HelpText = "The number of pop slices to use.")] + /// TODO: Again this is specific to a particular heuristic, is there a way to separate + /// inputs that are heuristic specific from those that are general? + /// One way may be to take a JSON as input that contains the inputs specific to the particular heuristic. + [Option("slices", Default = 2, HelpText = "Number of Pop partitions/slices")] public int PopSlices { get; set; } /// - /// The threshold for demand pinning. + /// The threshold for demand pinning heuristic. + /// Demands above this threshold are pinned to shortest path. /// - /// TODO: same as other comments that are about being heuristic specific. - [Option('t', "pinthreshold", Default = 5, HelpText = "The threshold for the demand pinning heuristic.")] + /// TODO: Same as other comments that are about being heuristic specific. + [Option('t', "pinthreshold", Default = 0.5, HelpText = "Threshold for demand pinning heuristic")] public double DemandPinningThreshold { get; set; } /// - /// The maximum number of paths to use for a demand. + /// The maximum number of paths to consider for each demand pair. + /// Higher values allow more routing flexibility but increase problem size. /// - /// TODO: same as others. - [Option('p', "paths", Default = 2, HelpText = "The maximum number of paths to use for any demand.")] + /// TODO: Same as others. + [Option('p', "paths", Default = 2, HelpText = "Maximum number of paths per demand pair")] public int Paths { get; set; } /// - /// The maximum shortest path length to pin in modified demandpinning. + /// The maximum shortest path length to pin in modified demand pinning. + /// Only applied when using ModifiedDp heuristic. + /// Value of -1 disables this constraint. /// - /// TODO: same as others. - [Option('p', "maxshortestlen", Default = -1, HelpText = "The maximum shortest path length to pin (only applied to ModifiedDp).")] + /// TODO: Same as others. + [Option("maxshortestlen", Default = -1, HelpText = "Maximum shortest path length to pin (ModifiedDp only, -1 to disable)")] public int MaxShortestPathLen { get; set; } /// - /// method for finding gap [search or direct]. + /// Method for finding the optimality gap. /// - /// TODO: expand on the comment to describe what each option does. - [Option('m', "method", Default = MethodChoice.Direct, HelpText = "the method for finding the desirable gap [Direct | Search | FindFeas | Random | HillClimber | SimulatedAnnealing]")] + /// + /// - Direct: Solve bilevel optimization directly for maximum gap + /// - Search: Binary search within interval [0, startinggap] for maximum gap + /// - FindFeas: Find any solution with gap >= startinggap + /// - Random: Random sampling to estimate gap distribution + /// - HillClimber: Local search with neighborhood exploration + /// - SimulatedAnnealing: Probabilistic local search with temperature cooling. + /// + /// TODO: Expand on the comment to describe what each option does. + [Option('m', "method", Default = MethodChoice.Direct, HelpText = "Gap-finding method [Direct | Search | FindFeas | Random | HillClimber | SimulatedAnnealing]")] public MethodChoice Method { get; set; } /// - /// if using search, shows how much close to optimal is ok. + /// Confidence level for Search method. + /// Search terminates when solution is within this fraction of optimal. /// - [Option('d', "confidence", Default = 0.1, HelpText = "if using search, will find a solution as close as this value to optimal.")] + [Option("confidence", Default = 0.1, HelpText = "Search terminates when within this fraction of optimal")] public double Confidencelvl { get; set; } /// - /// if using search, this values is used as the starting gap. + /// Starting gap value for Search and FindFeas methods. + /// Search uses this as upper bound, FindFeas uses as target threshold. /// - [Option('g', "startinggap", Default = 10, HelpText = "if using search, will start the search from this number.")] + [Option('g', "startinggap", Default = 10.0, HelpText = "Starting gap for Search/FindFeas methods")] public double StartingGap { get; set; } /// - /// an upper bound on all the demands to find more useful advers inputs. + /// Upper bound on all demand values. + /// Constrains adversarial inputs to realistic ranges. + /// Value of -1 means no upper bound. /// - /// TODO: is this also heuristic specific? if not, change the terminology to be general if yes, fix as stated above. - [Option('u', "demandub", Default = -1, HelpText = "an upper bound on all the demands.")] + /// TODO: Is this also heuristic specific? If not, change the terminology to be general. + /// If yes, fix as stated above. + [Option('u', "demandub", Default = -1.0, HelpText = "Upper bound on demand values (-1 for no bound)")] public double DemandUB { get; set; } /// - /// an upper bound on all the demands to find more useful advers inputs. + /// Maximum difference of total demands between partitions. + /// Used to balance load across partitions. + /// Value of -1 disables this constraint. /// - /// TODO: same as above. - [Option('x', "partitionSensitivity", Default = -1, HelpText = "the difference of total demands in each partition.")] + /// TODO: Same as above. + [Option("partitionSensitivity", Default = -1.0, HelpText = "Maximum demand difference between partitions (-1 to disable)")] public double PartitionSensitivity { get; set; } /// - /// number of trails for random search. + /// Number of trials for Random search or HillClimber. + /// More trials increase chance of finding better solutions. /// - [Option('n', "num", Default = 1, HelpText = "number of trials for random search or hill climber.")] + [Option('n', "num", Default = 1, HelpText = "Number of trials for Random/HillClimber")] public int NumRandom { get; set; } /// - /// number of neighbors to look. + /// Number of neighbors to evaluate before declaring local optimum. + /// Used by HillClimber and SimulatedAnnealing. /// - [Option('k', "neighbors", Default = 1, HelpText = "number of neighbors to search before marking as local optimum [for hill climber | simulated annealing].")] + [Option('k', "neighbors", Default = 1, HelpText = "Neighbors to check before local optimum [HillClimber | SimulatedAnnealing]")] public int NumNeighbors { get; set; } /// - /// initial temperature for simulated annealing. + /// Initial temperature for simulated annealing. + /// Higher values allow more exploration early in search. /// - [Option('t', "inittmp", Default = 1, HelpText = "initial temperature for simulated annealing.")] + [Option("inittmp", Default = 1.0, HelpText = "Initial temperature for simulated annealing")] public double InitTmp { get; set; } /// - /// initial temperature for simulated annealing. + /// Temperature decrease factor for simulated annealing. + /// Temperature is multiplied by this factor each iteration. /// - [Option('l', "lambda", Default = 1, HelpText = "temperature decrease factor for simulated annealing.")] + [Option('l', "lambda", Default = 1.0, HelpText = "Temperature decrease factor for simulated annealing")] public double TmpDecreaseFactor { get; set; } /// - /// max density of final traffic matrix. + /// Maximum density of the final traffic demand matrix. + /// Density = (non-zero demands) / (total possible demands). /// - /// TODO: same as other comments. - [Option('d', "maxdensity", Default = 1.0, HelpText = "maximum density of the final traffic demand.")] + /// TODO: Same as other comments. + [Option("maxdensity", Default = 1.0, HelpText = "Maximum density of traffic demand matrix (0.0-1.0)")] public double MaxDensity { get; set; } /// - /// max distance for large demands. + /// Maximum path distance for large demands. + /// Restricts large demands to nearby destinations. + /// Value of -1 disables this constraint. /// - /// TODO: same as other comments. - [Option('m', "maxdistancelarge", Default = -1, HelpText = "maximum distance for large demands.")] + /// TODO: Same as other comments. + [Option("maxdistancelarge", Default = -1, HelpText = "Maximum distance for large demands (-1 to disable)")] public int maxLargeDistance { get; set; } /// - /// max distance for small demands. + /// Maximum path distance for small demands. + /// Restricts small demands to nearby destinations. + /// Value of -1 disables this constraint. /// - /// TODO: same as other comments. - [Option('m', "maxdistancesmall", Default = -1, HelpText = "maximum distance for small demands.")] + /// TODO: Same as other comments. + [Option("maxdistancesmall", Default = -1, HelpText = "Maximum distance for small demands (-1 to disable)")] public int maxSmallDistance { get; set; } /// - /// Lower bound for large demands. + /// Lower bound to distinguish large demands from small demands. + /// Demands >= this value are considered "large". + /// Value of -1 disables large/small distinction. /// - /// TODO: same as other comments. - [Option('m', "largedemandlb", Default = -1, HelpText = "to distinguish large demands from small demands.")] + /// TODO: Same as other comments. + [Option("largedemandlb", Default = -1.0, HelpText = "Threshold to distinguish large vs small demands (-1 to disable)")] public double LargeDemandLB { get; set; } /// - /// enable clustering breakdown. + /// Enable hierarchical clustering for scalability. + /// Clusters the topology and optimizes inter/intra-cluster demands separately. + /// More scalable but may not find the globally optimal gap. /// - [Option('c', "enableclustering", Default = false, HelpText = "Use this input to enable clustering. Clustering is more scalable but does not find the best possible gap.")] + [Option("enableclustering", Default = false, HelpText = "Enable clustering for scalability (may not find optimal gap)")] public bool EnableClustering { get; set; } /// - /// cluster directory. + /// Directory containing cluster-level topology files. /// - /// TODO: not clear what this is doing, need a better user-visible and also private commment. - [Option('j', "clusterdir", Default = null, HelpText = "cluster lvl topo directory")] + /// TODO: Not clear what this is doing, need a better user-visible and also private comment. + /// Should contain JSON files defining the cluster structure and inter-cluster links. + [Option("clusterdir", Default = null, HelpText = "Directory containing cluster topology files")] public string ClusterDir { get; set; } /// - /// num clusters. + /// Number of clusters to create. /// - [Option('j', "numclusters", Default = null, HelpText = "number of clusters")] + [Option("numclusters", Default = 2, HelpText = "Number of clusters")] public int NumClusters { get; set; } /// - /// inter-cluster method version. + /// Version of clustering algorithm for inter-cluster demands. /// - /// TODO: what are the options? what is the difference between the different options? - [Option('j', "clusterversion", Default = 1, HelpText = "version of clustering for inter-cluster demands")] + /// TODO: What are the options? What is the difference between the different options? + /// v1, v2, v3 use different strategies for handling demands crossing cluster boundaries. + [Option("clusterversion", Default = 1, HelpText = "Clustering algorithm version for inter-cluster demands")] public int ClusterVersion { get; set; } /// - /// num inter-cluster samples. + /// Number of inter-cluster demand samples. /// - [Option('j', "interclustersamples", Default = 0, HelpText = "number of inter cluster samples")] + [Option("interclustersamples", Default = 0, HelpText = "Number of inter-cluster demand samples")] public int NumInterClusterSamples { get; set; } /// - /// num nodes per cluster for inter-cluster edges. + /// Number of nodes per cluster for inter-cluster edge representation. /// - [Option('j', "nodespercluster", Default = 0, HelpText = "number of nodes per cluster for inter-cluster edges")] + [Option("nodespercluster", Default = 0, HelpText = "Nodes per cluster for inter-cluster edges")] public int NumNodesPerCluster { get; set; } /// - /// inter-cluster quantization lvls. + /// Number of quantization levels for inter-cluster demands. + /// Only applies to clustering version 3. /// - /// TODO: unclear how one should use this parameter. - [Option('j', "numinterclusterquantization", Default = -1, HelpText = "inter-cluster demands number of quantizations [only works for v3].")] + /// TODO: Unclear how one should use this parameter. + [Option("numinterclusterquantization", Default = -1, HelpText = "Inter-cluster demand quantization levels (v3 only)")] public int NumInterClusterQuantizations { get; set; } /// - /// error analysis. + /// Run full optimization after initial clustering solution. + /// Uses clustered solution as starting point for full-scale optimization. + /// Requires clustering to be enabled and PrimalDual inner encoding. /// - /// TODO: not fully clear what this does, needs a better comment both internally and user-visible. - [Option('j', "fullopt", Default = false, HelpText = "after finding the demand, will run the full optimization with demands as init point.")] + /// TODO: Not fully clear what this does, needs a better comment both internally and user-visible. + [Option("fullopt", Default = false, HelpText = "Run full optimization with clustered solution as init point")] public bool FullOpt { get; set; } /// - /// error analysis timer. + /// Timeout for full optimization phase. + /// Value of -1 uses the main timeout. /// - [Option('j', "fullopttimer", Default = -1, HelpText = "the duration to run the full optimization for error analysis.")] + [Option("fullopttimer", Default = -1.0, HelpText = "Timeout for full optimization phase (-1 uses main timeout)")] public double FullOptTimer { get; set; } /// - /// gurobi improve upper bound instead of bestObj. + /// Focus on improving upper bound after initial solution. + /// Runs additional optimization phase targeting the dual bound. /// - [Option('j', "ubfocus", Default = false, HelpText = "if enabled after finding demand, will solve another optimization focusing on improving upper bound.")] + [Option("ubfocus", Default = false, HelpText = "Run additional phase focusing on upper bound improvement")] public bool UBFocus { get; set; } /// - /// gurobi upper bound timer. + /// Timeout for upper bound focus phase. + /// Value of -1 uses the main timeout. /// - [Option('j', "ubfocustimeout", Default = -1, HelpText = "the timer of solver when focusing on ub.")] + [Option("ubfocustimeout", Default = -1.0, HelpText = "Timeout for upper bound focus phase (-1 uses main timeout)")] public double UBFocusTimer { get; set; } /// - /// num processes. + /// Number of parallel processes to use. + /// Value of -1 uses system default. /// - /// TODO: for what? - [Option('s', "numProcesses", Default = -1, HelpText = "num processes to use for.")] + /// TODO: For what? Parallel optimization? Parallel validation? + [Option("numProcesses", Default = -1, HelpText = "Number of parallel processes (-1 for system default)")] public int NumProcesses { get; set; } /// - /// seed. + /// Random seed for reproducibility. + /// Used by Random, HillClimber, SimulatedAnnealing methods. /// - [Option('s', "seed", Default = 1, HelpText = "seed for random generator.")] + [Option("seed", Default = 1, HelpText = "Random seed for reproducibility")] public int Seed { get; set; } /// - /// seed. + /// Standard deviation for neighbor generation in HillClimber. + /// Controls the size of random perturbations when exploring neighbors. /// - [Option('b', "stddev", Default = 100, HelpText = "standard deviation for generating neighbor for hill climber.")] + [Option('b', "stddev", Default = 100, HelpText = "Standard deviation for neighbor generation [HillClimber]")] public int StdDev { get; set; } /// - /// store trajectory. + /// Store optimization progress trajectory. + /// Saves intermediate solutions for analysis. /// - [Option('m', "storeprogress", Default = false, HelpText = "store the progress for the specified approach.")] + [Option("storeprogress", Default = false, HelpText = "Store optimization progress trajectory")] public bool StoreProgress { get; set; } /// - /// file to read paths. + /// File containing pre-computed paths to use. /// - /// TODO: heuristic specific. It would also be good to specify what format the file has to have. - [Option('m', "pathfile", Default = null, HelpText = "file to read the paths from.")] + /// TODO: Heuristic specific. It would also be good to specify what format the file has to have. + /// Expected format: JSON with paths per source-destination pair. + [Option("pathfile", Default = null, HelpText = "File containing pre-computed paths (JSON format)")] public string PathFile { get; set; } /// - /// log file. + /// Path to log file for storing progress. /// - [Option('t', "logfile", Default = null, HelpText = "path to the log file to store the progress.")] + [Option("logfile", Default = null, HelpText = "Path to log file for progress storage")] public string LogFile { get; set; } /// - /// Whether to print debugging information. + /// Factor to downscale the optimization problem. + /// Reduces problem size for faster experimentation. + /// All capacities and demands are multiplied by this factor. /// - [Option('d', "debug", Default = false, HelpText = "Prints debugging messages to standard output.")] - public bool Debug { get; set; } + [Option("downscale", Default = 1.0, HelpText = "Factor to downscale problem size (1.0 = no scaling)")] + public double DownScaleFactor { get; set; } /// - /// To downscale the solver. + /// Number of threads for Gurobi solver. + /// Value of 0 lets Gurobi choose automatically. + /// Value of 1 ensures deterministic results but slower execution. /// - [Option('p', "downscale", Default = 1.0, HelpText = "Factor to downscale MetaOpt.")] - public double DownScaleFactor { get; set; } + [Option("gurobithreads", Default = 1, HelpText = "Gurobi threads (0=auto, 1=deterministic)")] + public int NumGurobiThreads { get; set; } + + #endregion + + #region Bin Packing Options /// - /// number of threads to use in gurobi. + /// Number of bins available for packing. + /// The adversarial generator tries to find items that use many bins with FFD + /// while requiring fewer bins optimally. /// - [Option('t', "gurobithreads", Default = 0, HelpText = "number of threads to use for Gurobi.")] - public int NumGurobiThreads { get; set; } + [Option("numBins", Default = 6, HelpText = "Number of bins available (BinPacking)")] + public int NumBins { get; set; } /// - /// to show more detailed logs. + /// Number of items/demands to pack. + /// More items = larger search space for adversarial inputs. /// - [Option('v', "verbose", Default = false, HelpText = "more detailed logs")] - public bool Verbose { get; set; } + [Option("numDemands", Default = 9, HelpText = "Number of items to pack (BinPacking)")] + public int NumDemands { get; set; } + + /// + /// Number of dimensions for vector bin packing. + /// Each item has size in each dimension, bins have capacity per dimension. + /// + [Option("numDimensions", Default = 2, HelpText = "Number of dimensions (BinPacking)")] + public int NumDimensions { get; set; } + + /// + /// Target number of bins for optimal solution. + /// Adversarial generator finds items that pack optimally in this many bins + /// but require more bins with FFD heuristic. + /// + [Option("optimalBins", Default = 3, HelpText = "Target optimal bin count (BinPacking)")] + public int OptimalBins { get; set; } + + /// + /// First-Fit variant to use as the heuristic. + /// + /// + /// - FF: First Fit (no sorting, place in first bin that fits) + /// - FFDSum: First Fit Decreasing by sum of dimensions + /// - FFDProd: First Fit Decreasing by product of dimensions + /// - FFDDiv: First Fit Decreasing by division of dimensions (2D only). + /// + [Option("ffMethod", Default = FFDMethodChoice.FFDSum, HelpText = "First-Fit variant: FF, FFDSum, FFDProd, FFDDiv")] + public FFDMethodChoice FFMethod { get; set; } + + /// + /// Enable symmetry breaking constraints. + /// Reduces search space by eliminating equivalent solutions. + /// May speed up optimization but could miss some adversarial inputs. + /// + [Option("breakSymmetry", Default = "false", HelpText = "Enable symmetry breaking in BinPacking encoder (true/false)")] + public string BreakSymmetryStr { get; set; } + + /// + /// Enable symmetry breaking constraints. + /// Reduces search space by eliminating equivalent solutions. + /// May speed up optimization but could miss some adversarial inputs. + /// + /// + /// If enabled, we leverage symmetry to reduce the optimization size and enhance the scalability of optimal bin packing. + /// + public bool BreakSymmetry => + string.Equals(BreakSymmetryStr, "true", StringComparison.OrdinalIgnoreCase); + + /// + /// Bin capacities per dimension (comma-separated). + /// Each value is the capacity for one dimension. + /// Values slightly above 1.0 (e.g., 1.00001) avoid numerical issues. + /// + [Option("binCapacity", Default = "1.00001,1.00001", HelpText = "Comma-separated bin capacities per dimension")] + public string BinCapacity { get; set; } + + #endregion + + #region PIFO Options + + /// + /// First PIFO method (h1 encoder). + /// + [Option("pifoMethod1", Default = PIFOMethodChoice.SPPIFO, HelpText = "First PIFO method: PIFO, SPPIFO, AIFO, ModifiedSPPIFO")] + public PIFOMethodChoice PIFOMethod1 { get; set; } + + /// + /// Second PIFO method (h2 encoder). + /// + [Option("pifoMethod2", Default = PIFOMethodChoice.AIFO, HelpText = "Second PIFO method: PIFO, SPPIFO, AIFO, ModifiedSPPIFO")] + public PIFOMethodChoice PIFOMethod2 { get; set; } + + /// + /// Whether to consider packet drop in PIFO scheduling. + /// + [Option("considerPktDrop", Default = true, HelpText = "Consider packet drop (true/false)")] + public bool ConsiderPktDrop { get; set; } + + /// + /// Split queue parameter for ModifiedSPPIFO. + /// + [Option("splitQueue", Default = 4, HelpText = "Split queue count for ModifiedSPPIFO")] + public int SplitQueue { get; set; } + + /// + /// Split rank parameter for ModifiedSPPIFO. + /// + [Option("splitRank", Default = 100, HelpText = "Split rank threshold for ModifiedSPPIFO")] + public int SplitRank { get; set; } + + /// + /// Number of packets in the sequence. + /// More packets = larger adversarial search space. + /// + [Option("numPackets", Default = 18, HelpText = "Number of packets (PIFO)")] + public int NumPackets { get; set; } + + /// + /// Maximum rank value for packet priorities. + /// Lower rank = higher priority. + /// + [Option("maxRank", Default = 8, HelpText = "Maximum rank value (PIFO)")] + public int MaxRank { get; set; } + + /// + /// Number of priority queues for SP-PIFO. + /// Packets are mapped to queues based on rank. + /// + [Option("numQueues", Default = 4, HelpText = "Number of queues for SP-PIFO")] + public int NumQueues { get; set; } + + /// + /// Maximum size of each queue. + /// Packets are dropped if queue is full. + /// + [Option("maxQueueSize", Default = 12, HelpText = "Maximum queue size (PIFO)")] + public int MaxQueueSize { get; set; } + + /// + /// Window size for AIFO admission control. + /// AIFO admits packets based on rank relative to recent window. + /// + [Option("windowSize", Default = 12, HelpText = "AIFO window size (PIFO)")] + public int WindowSize { get; set; } + + /// + /// Burst tolerance parameter for AIFO. + /// Controls how much burst traffic is allowed. + /// + [Option("burstParam", Default = 0.1, HelpText = "AIFO burst parameter (PIFO)")] + public double BurstParam { get; set; } + + #endregion + + #region Failure Analysis Options + + /// + /// Use built-in default topology instead of loading from file. + /// Default topology is a simple 4-node diamond for testing. + /// + [Option("useDefaultTopology", Default = "true", HelpText = "Use built-in default topology for Failure Analysis. Requires value: true or false")] + public string UseDefaultTopologyStr { get; set; } + + /// + /// Use built-in default topology instead of loading from file. + /// Default topology is a simple 4-node diamond for testing. + /// + public bool UseDefaultTopology => + string.Equals(UseDefaultTopologyStr, "true", StringComparison.OrdinalIgnoreCase); + + /// + /// Maximum number of simultaneous link failures to consider. + /// Higher values explore more failure scenarios but increase problem size. + /// + [Option("maxNumFailures", Default = 1, HelpText = "Maximum simultaneous link failures")] + public int MaxNumFailures { get; set; } + + /// + /// Number of extra paths for rerouting under failures. + /// More paths = more rerouting options but larger problem. + /// + [Option("numExtraPaths", Default = 1, HelpText = "Extra paths for failure rerouting")] + public int NumExtraPaths { get; set; } + + /// + /// Minimum failure probability for considering a link. + /// Links with probability below this threshold are assumed not to fail. + /// + [Option("failureProbThreshold", Default = 0.25, HelpText = "Minimum failure probability to consider")] + public double FailureProbThreshold { get; set; } + + /// + /// Minimum scenario probability threshold. + /// Failure scenarios with combined probability below this are ignored. + /// + [Option("scenarioProbThreshold", Default = 0.0, HelpText = "Minimum failure scenario probability")] + public double ScenarioProbThreshold { get; set; } + + #endregion + } + #region Enums + /// - /// The encoding heuristic. + /// Problem type to solve. + /// + public enum ProblemType + { + /// + /// Traffic engineering: Find worst-case demand patterns for routing heuristics. + /// + TrafficEngineering, + + /// + /// Bin packing: Find item sizes that maximize FFD vs optimal gap. + /// + BinPacking, + + /// + /// PIFO: Find packet sequences that maximize scheduling inversions. + /// + PIFO, + + /// + /// Failure analysis: Find failure scenarios that maximize throughput degradation. + /// + FailureAnalysis, + } + + /// + /// The encoding heuristic for traffic engineering. /// public enum Heuristic { /// - /// The pop heuristic. + /// Partition-based routing with random demand partitioning. /// Pop, /// - /// The average pop heuristic over multiple sample + /// Average Pop performance over multiple partition samples. /// ExpectedPop, /// - /// The threshold heuristic. + /// Threshold-based path selection (pin large demands to shortest path). /// DemandPinning, /// - /// Combine POP and DP. + /// Combined Pop and DemandPinning. /// PopDp, /// - /// The average gap of running POP and DP in parallel. + /// Average gap of running Pop and DemandPinning in parallel. /// ExpectedPopDp, /// - /// Parallel POP. Running multiple instances of POP in parallel. + /// Multiple Pop instances running in parallel. /// ParallelPop, /// - /// Running multiple instances of POP in parallel with DP. + /// Multiple Pop instances in parallel with DemandPinning. /// ParallelPopDp, /// - /// Modified Demand Pinning with upper bound of pinned path lengths. + /// DemandPinning with upper bound on pinned path lengths. /// ModifiedDp, } + /// - /// The solver we want to use. + /// The solver to use for optimization. /// public enum SolverChoice { /// - /// The Gurobi solver. + /// Gurobi solver (MIP - Mixed Integer Programming). + /// Commercial solver, fast, requires license. /// Gurobi, /// - /// The Zen solver. + /// Zen solver (SMT - Satisfiability Modulo Theories). + /// Based on Z3, open source, good for constraint satisfaction. /// Zen, + + /// + /// OR-Tools solver (CP-SAT - Constraint Programming with SAT). + /// Google's open-source optimization suite, good for combinatorial optimization. + /// + OrTools, } + /// - /// The method we want to use. + /// Method for finding the optimality gap. /// public enum MethodChoice { /// - /// directly find the max gap + /// Directly solve bilevel optimization for maximum gap. + /// Most accurate but may be slow for large problems. /// Direct, + /// - /// search for the max gap with some interval + /// Binary search within interval for maximum gap. + /// Faster than Direct for some problems. /// Search, + /// - /// find a solution with gap at least equal to startinggap. + /// Find any feasible solution with gap >= startinggap. + /// Fastest when you only need to prove a gap exists. /// FindFeas, + /// - /// find a solution with random search. + /// Random sampling to estimate gap distribution. + /// Good for understanding gap landscape. /// Random, + /// - /// find a solution with hill climber. + /// Local search with neighborhood exploration. + /// May find good solutions faster than Direct. /// HillClimber, + /// - /// find a solution with simulated annealing. + /// Probabilistic local search with temperature cooling. + /// Can escape local optima better than HillClimber. /// SimulatedAnnealing, } -} + + /// + /// PIFO method choices for scheduling algorithms. + /// + public enum PIFOMethodChoice + { + /// + /// Push-In First-Out: Packets are inserted based on rank and dequeued from the head. + /// + PIFO, + + /// + /// Strict Priority PIFO: Uses multiple priority queues to approximate PIFO behavior. + /// + SPPIFO, + + /// + /// Approximate In-Order First-Out: Uses shallow buffers with admission control. + /// Only valid when ConsiderPktDrop is true. + /// + AIFO, + + /// + /// Modified SP-PIFO: Variant with configurable queue splitting via splitQueue and splitRank parameters. + /// Only valid when ConsiderPktDrop is false. + /// + ModifiedSPPIFO, + } + + #endregion +} \ No newline at end of file diff --git a/MetaOptimize.Cli/FailureAnalysisRunner.cs b/MetaOptimize.Cli/FailureAnalysisRunner.cs new file mode 100644 index 000000000..68e1538b5 --- /dev/null +++ b/MetaOptimize.Cli/FailureAnalysisRunner.cs @@ -0,0 +1,218 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace MetaOptimize.Cli +{ + using System.Diagnostics; + using Google.OrTools.LinearSolver; + using Gurobi; + using MetaOptimize.FailureAnalysis; + using ZenLib; + using ZenLib.ModelChecking; + + /// + /// Runner for network failure analysis adversarial optimization. + /// Finds failure scenarios that maximize the gap between normal and degraded network performance. + /// + /// + /// Analyzes network resilience by finding worst-case link failure combinations. + /// Compares optimal routing under normal conditions against routing under failure scenarios, + /// identifying vulnerabilities where failures cause significant throughput degradation. + /// + /// Supports configurable failure probability thresholds and multiple simultaneous failures. + /// + public static class FailureAnalysisRunner + { + /// + /// Runs failure analysis adversarial optimization. + /// Dispatches to the appropriate solver-specific implementation. + /// + /// Command-line options containing failure analysis parameters. + /// Thrown when an unsupported solver is specified. + public static void Run(CliOptions opts) + { + switch (opts.SolverChoice) + { + case SolverChoice.OrTools: + FailureAnalysisRunnerImpl.CreateSolver = () => new ORToolsSolver(); + FailureAnalysisRunnerImpl.Run(opts); + break; + case SolverChoice.Gurobi: + FailureAnalysisRunnerImpl.CreateSolver = + () => new GurobiSOS(verbose: Convert.ToInt32(opts.Verbose), timeout: opts.Timeout); + FailureAnalysisRunnerImpl.Run(opts); + break; + case SolverChoice.Zen: + FailureAnalysisRunnerImpl, ZenSolution>.CreateSolver = + () => new SolverZen(); + FailureAnalysisRunnerImpl, ZenSolution>.Run(opts); + break; + default: + throw new Exception($"Unsupported solver: {opts.SolverChoice}. Valid options: OrTools, Gurobi, Zen"); + } + } + } + + /// + /// Generic implementation of failure analysis adversarial optimization. + /// + /// Solver variable type (GRBVar or Zen). + /// Solver solution type (GRBModel or ZenSolution). + /// + /// Uses bilevel optimization to find: + /// 1. Outer level: Failure scenario (which links fail) + /// 2. Inner level: Optimal routing under that failure scenario + /// + /// The gap represents how much throughput is lost due to the failure. + /// + internal sealed class FailureAnalysisRunnerImpl + { + /// + /// Factory function to create solver instances. + /// Set by the dispatcher before calling Run(). + /// + internal static Func> CreateSolver = null; + + /// + /// Creates a default 4-node diamond topology for testing. + /// + /// A simple test topology with nodes a, b, c, d. + /// + /// Topology structure: + /// a + /// /|\ + /// / | \ + /// b--+--c + /// \ | / + /// \|/ + /// d + /// + /// Link capacities vary to create interesting failure scenarios. + /// + private static Topology CreateDefaultFailureTopology() + { + var topology = new Topology(); + topology.AddNode("a"); + topology.AddNode("b"); + topology.AddNode("c"); + topology.AddNode("d"); + topology.AddEdge("a", "b", capacity: 10); + topology.AddEdge("a", "c", capacity: 10); + topology.AddEdge("b", "d", capacity: 10); + topology.AddEdge("c", "d", capacity: 10); + topology.AddEdge("a", "d", capacity: 5); // Direct path with lower capacity + topology.AddEdge("b", "c", capacity: 3); // Cross-link with lowest capacity + return topology; + } + + /// + /// Runs failure analysis adversarial optimization. + /// + /// Command-line options containing failure analysis parameters. + /// + /// Key parameters from opts: + /// - UseDefaultTopology: Use built-in test topology or load from file + /// - MaxNumFailures: Maximum simultaneous link failures to consider + /// - NumExtraPaths: Additional paths for rerouting under failures + /// - FailureProbThreshold: Minimum probability for considering a failure + /// - InnerEncoding: KKT or PrimalDual for inner optimization + /// - DemandList: Quantized demand levels for optimization. + /// + internal static void Run(CliOptions opts) + { + Console.WriteLine($"Max Failures: {opts.MaxNumFailures}, Extra Paths: {opts.NumExtraPaths}"); + Console.WriteLine($"Failure Prob Threshold: {opts.FailureProbThreshold}"); + + // Load or create topology + Topology topology; + List clusters = null; + if (opts.UseDefaultTopology) + { + topology = CreateDefaultFailureTopology(); + Console.WriteLine("Using default test topology"); + } + else + { + (topology, clusters) = CliUtils.getTopology( + opts.TopologyFile, + opts.PathFile, + opts.DownScaleFactor, + opts.EnableClustering, + opts.NumClusters, + opts.ClusterDir, + opts.Verbose); + Console.WriteLine($"Loaded topology from: {opts.TopologyFile}"); + } + + // Default demand matrix for test topology + var demands = new Dictionary<(string, string), double> + { + { ("a", "d"), 10 }, + { ("b", "d"), 5 }, + { ("a", "c"), 5 }, + { ("c", "d"), 0 }, + { ("a", "b"), 0 }, + { ("b", "c"), 0 }, + }; + + // Parse demand quantization levels + var demandSet = new HashSet(opts.DemandList.Split(',').Select(double.Parse)); + var demandList = new GenericList(demandSet); + + // Link failure probabilities for test topology + var probs = new Dictionary<(string, string), double> + { + { ("a", "d"), 0.3 }, // Direct link has highest failure probability + { ("b", "d"), 0.2 }, + { ("a", "c"), 0 }, + { ("a", "b"), 0 }, + { ("c", "d"), 0 }, + { ("b", "c"), 0 }, + }; + + var solver = CreateSolver(); + + var timer = Stopwatch.StartNew(); + + // Create encoders for normal and failure scenarios + var optimalEncoder = new TEMaxFlowOptimalEncoder(solver, maxNumPaths: 2); + var failureEncoder = new FailureAnalysisEncoder(solver, maxNumPathTotal: 2); + var adversarialGenerator = new FailureAnalysisAdversarialGenerator( + topology, maxNumPaths: 2); + + // Find worst-case failure scenario + var (optimalSol, failureSol) = adversarialGenerator.MaximizeOptimalityGap( + optimalEncoder, failureEncoder, + innerEncoding: opts.InnerEncoding, + constrainedDemands: demands, + maxNumFailures: opts.MaxNumFailures, + demandList: demandList, + numExtraPaths: opts.NumExtraPaths, + lagFailureProbabilities: probs, + failureProbThreshold: opts.FailureProbThreshold); + + timer.Stop(); + + // Display results + Console.WriteLine("\n" + new string('=', 60)); + Console.WriteLine("RESULTS:"); + Console.WriteLine($"Optimal objective (no failures): {optimalSol.MaxObjective}"); + Console.WriteLine($"Failure scenario objective: {failureSol.MaxObjective}"); + Console.WriteLine($"Gap (throughput loss): {optimalSol.MaxObjective - failureSol.MaxObjective}"); + Console.WriteLine($"Time: {timer.ElapsedMilliseconds}ms"); + Console.WriteLine(new string('=', 60)); + + // Verbose output: full solution details + if (opts.Verbose) + { + Console.WriteLine("\nOptimal Solution (no failures):"); + Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject( + optimalSol, Newtonsoft.Json.Formatting.Indented)); + Console.WriteLine("\nFailure Scenario Solution:"); + Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject( + failureSol, Newtonsoft.Json.Formatting.Indented)); + } + } + } +} \ No newline at end of file diff --git a/MetaOptimize.Cli/MainEntry.cs b/MetaOptimize.Cli/MainEntry_old.cs similarity index 92% rename from MetaOptimize.Cli/MainEntry.cs rename to MetaOptimize.Cli/MainEntry_old.cs index 0d93893bb..6dac6b34d 100644 --- a/MetaOptimize.Cli/MainEntry.cs +++ b/MetaOptimize.Cli/MainEntry_old.cs @@ -10,17 +10,18 @@ namespace MetaOptimize.Cli using Gurobi; using MetaOptimize; using ZenLib; + using ZenLib.ModelChecking; /// /// Main entry point for the program. /// - public class MainEntry + public class MainEntry_old { /// /// checks whether we get the solution we expect after running the solvers. /// /// - public static void TEExampleMain(string[] args) + public static void MainOld(string[] args) { var topology = new Topology(); topology.AddNode("a"); @@ -63,7 +64,7 @@ public static void TEExampleMain(string[] args) /// Use this function to test our theorem for VBP. /// (see theorem 1 in our NSDI24 Paper). /// - public static void vbpMain(string[] args) + public static void vbMain(string[] args) { // OPT = 2m + 3n // HUE = 4m + 6n @@ -188,7 +189,7 @@ public static void vbpMain(string[] args) /// test MetaOpt on VBP. /// /// TODO: specify how this function is different from the previous. - public static void Main(string[] args) + public static void MainVBP(string[] args) { var binSize = new List(); binSize.Add(1.00001); @@ -352,53 +353,6 @@ private static int ComputeInversionNum(PIFOOptimizationSolution optimalSolutionG return numInvOpt; } - /// - /// Experiments for NSDI. - /// - public static void NSDIMain(string[] args) - { - // NSDIExp.compareGapDelayDiffMethodsDP(); - // NSDIExp.compareLargeScaleGapDelayDiffMethodsDP(); - // NSDIExp.compareGapDelayDiffMethodsPop(); - // NSDIExp.AblationStudyClusteringOnDP(); - // NSDIExp.BlackBoxParameterTunning(); - NSDIExp.AddRealisticConstraintsDP(); - // NSDIExp.gapThresholdDemandPinningForDifferentTopologies(); - // NSDIExp.ImpactNumPathsPartitionsExpectedPop(); - // NSDIExp.AblationStudyClusteringOnDP(); - // NSDIExp.BlackBoxParameterTunning(); - // NSDIExp.AnalyzeModifiedDP(); - // NSDIExp.ImpactNumNodesRadixSmallWordTopoDemandPinning(); - // NSDIExp.ImpactNumSamplesExpectedPop(); - // NSDIExp.AnalyzeParallelHeuristics(); - } - - /// - /// Experiments for hotnets. - /// - public static void hotnetsMain(string[] args) - { - // var topology = Topology.RandomRegularGraph(8, 7, 1, seed: 0); - // var topology = Topology.SmallWordGraph(5, 4, 1); - // foreach (var edge in topology.GetAllEdges()) { - // Console.WriteLine(edge.Source + "_" + edge.Target); - // } - // foreach (var pair in topology.GetNodePairs()) { - // if (!topology.ContaintsEdge(pair.Item1, pair.Item2, 1)) { - // Console.WriteLine("missing link " + pair.Item1 + " " + pair.Item2); - // } - // } - // Experiment.printPaths(); - // HotNetsExperiment.impactOfDPThresholdOnGap(); - // Experiment.ImpactNumPathsDemandPinning(); - // Experiment.ImpactNumNodesRadixRandomRegularGraphDemandPinning(); - HotNetsExperiment.impactSmallWordGraphParamsDP(); - // Experiment.ImpactNumPathsPartitionsPop(); - // Experiment.compareGapDelayDiffMethodsPop(); - // Experiment.compareGapDelayDiffMethodsDP(); - // Experiment.compareTopoSizeLatency(); - } - /// /// Main entry point for the program. /// The function takes the command line arguments and stores them in a diff --git a/MetaOptimize.Cli/MetaOptimize.Cli.csproj b/MetaOptimize.Cli/MetaOptimize.Cli.csproj index ebeae9c2e..97cc9e6d5 100644 --- a/MetaOptimize.Cli/MetaOptimize.Cli.csproj +++ b/MetaOptimize.Cli/MetaOptimize.Cli.csproj @@ -1,31 +1,22 @@  - + Exe - net6.0 - enable - disable - x64 - x64 - MetaOptimize.Cli.MainEntry + MetaOptimize.Cli.Program - - - - - - - - - - - + + + + + + + + - - + \ No newline at end of file diff --git a/MetaOptimize.Cli/PIFORunner.cs b/MetaOptimize.Cli/PIFORunner.cs new file mode 100644 index 000000000..f3c7fda36 --- /dev/null +++ b/MetaOptimize.Cli/PIFORunner.cs @@ -0,0 +1,244 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace MetaOptimize.Cli +{ + using System.Diagnostics; + + /// + /// Runner for PIFO (Push-In First-Out) packet scheduling adversarial optimization. + /// Finds packet arrival patterns that maximize scheduling inversions between SP-PIFO and AIFO. + /// + /// + /// Compares two packet scheduling algorithms: + /// - SP-PIFO with Drop: Strict Priority PIFO with packet dropping under congestion + /// - AIFO: Approximate Ideal Fair Ordering with window-based admission + /// + /// The adversarial generator finds packet rank sequences where AIFO produces + /// significantly more inversions (out-of-order deliveries) than SP-PIFO. + /// + /// An inversion occurs when a lower-priority packet is scheduled before a + /// higher-priority packet that arrived earlier. + /// + public sealed class PIFORunner + { + /// + /// Computes the number of inversions for a single packet. + /// An inversion occurs when a lower-priority packet precedes this packet in the schedule. + /// + /// The scheduling solution to analyze. + /// Mapping from schedule order to packet rank. + /// The packet ID to compute inversions for. + /// Number of inversions for the specified packet. + /// + /// For admitted packets: counts how many packets scheduled earlier have higher rank values + /// (lower priority, since lower rank = higher priority). + /// For dropped packets: counts inversions against all admitted packets. + /// + private static int ComputeInversionNum( + PIFOOptimizationSolution solution, + Dictionary orderToRank, + int pid) + { + int numInv = 0; + + // Check if packet was admitted (0.98 threshold handles floating-point) + if (solution.Admit[pid] >= 0.98) + { + int currOrder = solution.Order[pid]; + // Count packets scheduled earlier with worse priority (higher rank) + for (int prev = 0; prev < currOrder; prev++) + { + if (orderToRank[prev] > solution.Ranks[pid]) + { + numInv += 1; + } + } + } + else + { + // Dropped packet: count against all admitted packets with worse priority + foreach (var (order, rank) in orderToRank) + { + if (rank > solution.Ranks[pid]) + { + numInv += 1; + } + } + } + + return numInv; + } + + /// + /// Computes total inversion counts for both optimal and heuristic solutions. + /// + /// The optimal (SP-PIFO) scheduling solution. + /// The heuristic (AIFO) scheduling solution. + /// Total number of packets in the sequence. + /// Tuple of (optimal inversions, heuristic inversions). + private static (int optimal, int heuristic) ComputeInversions( + PIFOOptimizationSolution optimalSol, + PIFOOptimizationSolution heuristicSol, + int numPackets) + { + // Build order-to-rank mappings for admitted packets + var orderToRankOpt = new Dictionary(); + var orderToRankHeu = new Dictionary(); + + for (int pid = 0; pid < numPackets; pid++) + { + if (optimalSol.Admit[pid] == 1) + { + orderToRankOpt[optimalSol.Order[pid]] = optimalSol.Ranks[pid]; + } + + if (heuristicSol.Admit[pid] == 1) + { + orderToRankHeu[heuristicSol.Order[pid]] = heuristicSol.Ranks[pid]; + } + } + + // Sum inversions across all packets + int numInvOpt = 0; + int numInvHeu = 0; + + for (int pid = 0; pid < numPackets; pid++) + { + numInvOpt += ComputeInversionNum(optimalSol, orderToRankOpt, pid); + numInvHeu += ComputeInversionNum(heuristicSol, orderToRankHeu, pid); + } + + return (numInvOpt, numInvHeu); + } + + /// + /// Creates the appropriate PIFO encoder based on method and packet drop settings. + /// + /// The PIFO scheduling method to use. + /// The solver instance. + /// CLI options containing encoder parameters. + /// Configured encoder implementing IEncoder interface. + /// + /// Thrown when AIFO is used without packet drop, or ModifiedSPPIFO is used with packet drop. + /// + private static IEncoder CreateEncoder( + PIFOMethodChoice method, + ISolver solver, + CliOptions opts) + { + return method switch + { + PIFOMethodChoice.PIFO => opts.ConsiderPktDrop + ? new PIFOWithDropAvgDelayEncoder(solver, opts.NumPackets, opts.MaxRank, opts.MaxQueueSize) + : new PIFOAvgDelayOptimalEncoder(solver, opts.NumPackets, opts.MaxRank), + + PIFOMethodChoice.SPPIFO => opts.ConsiderPktDrop + ? new SPPIFOWithDropAvgDelayEncoder(solver, opts.NumPackets, opts.NumQueues, opts.MaxRank, opts.MaxQueueSize) + : new SPPIFOAvgDelayEncoder(solver, opts.NumPackets, opts.NumQueues, opts.MaxRank), + + PIFOMethodChoice.AIFO => opts.ConsiderPktDrop + ? new AIFOAvgDelayEncoder(solver, opts.NumPackets, opts.MaxRank, opts.MaxQueueSize, opts.WindowSize, opts.BurstParam) + : throw new ArgumentException( + "AIFO only works on shallow buffers and decides whether to admit or drop the packet. " + + "As a result, it does not apply to cases where we do not want packet drop."), + + PIFOMethodChoice.ModifiedSPPIFO => !opts.ConsiderPktDrop + ? new ModifiedSPPIFOAvgDelayEncoder(solver, opts.NumPackets, opts.SplitQueue, opts.NumQueues, opts.SplitRank, opts.MaxRank) + : throw new ArgumentException( + "ModifiedSPPIFO does not support packet drop yet."), + + _ => throw new ArgumentException($"Unknown PIFO method: {method}") + }; + } + + /// + /// Runs PIFO packet scheduling adversarial optimization. + /// + /// CLI options. + /// Thrown for unsupported solver. + public static void Run(CliOptions opts) + { + switch (opts.SolverChoice) + { + case SolverChoice.OrTools: + RunWithSolver(new ORToolsSolver(), opts); + break; + case SolverChoice.Zen: + RunWithSolver(new SolverZen(), opts); + break; + case SolverChoice.Gurobi: + RunWithSolver(new GurobiSOS(timeout: opts.Timeout, verbose: Convert.ToInt32(opts.Verbose)), opts); + break; + default: + throw new ArgumentException($"Unsupported solver: {opts.SolverChoice}. Valid options: OrTools, Gurobi, Zen"); + } + } + + /// + /// Runs PIFO packet scheduling adversarial optimization with the specified solver. + /// + /// + /// Creates encoders for the selected PIFO methods, then uses adversarial + /// optimization to find packet rank sequences that maximize the cost gap. + /// + /// Key parameters from opts: + /// - NumPackets: Total packets in sequence + /// - MaxRank: Maximum priority rank value + /// - NumQueues: Number of queues for SP-PIFO + /// - MaxQueueSize: Maximum packets per queue + /// - WindowSize: AIFO admission window size + /// - BurstParam: AIFO burst tolerance parameter. + /// + private static void RunWithSolver(ISolver solver, CliOptions opts) + { + Console.WriteLine($"Packets: {opts.NumPackets}, Max Rank: {opts.MaxRank}, Queues: {opts.NumQueues}"); + Console.WriteLine($"Max Queue Size: {opts.MaxQueueSize}, Window Size: {opts.WindowSize}"); + Console.WriteLine($"PIFO Method 1: {opts.PIFOMethod1} (ConsiderPktDrop={opts.ConsiderPktDrop})"); + Console.WriteLine($"PIFO Method 2: {opts.PIFOMethod2} (ConsiderPktDrop={opts.ConsiderPktDrop})"); + + // Create encoders based on CLI options + var h1 = CreateEncoder(opts.PIFOMethod1, solver, opts); + var h2 = CreateEncoder(opts.PIFOMethod2, solver, opts); + + // Create adversarial generator + var adversarialGenerator = new PIFOAdversarialInputGenerator( + opts.NumPackets, opts.MaxRank); + + var timer = Stopwatch.StartNew(); + + // Find worst-case packet sequence + var (optimalSolution, heuristicSolution) = adversarialGenerator.MaximizeOptimalityGap( + h1, h2, verbose: opts.Verbose); + + timer.Stop(); + + // Compute inversion metrics + var (numInvOpt, numInvHeu) = ComputeInversions( + optimalSolution, heuristicSolution, opts.NumPackets); + + // Display results + Console.WriteLine("\n" + new string('=', 60)); + Console.WriteLine("RESULTS:"); + Console.WriteLine($"{opts.PIFOMethod1} cost: {optimalSolution.Cost}"); + Console.WriteLine($"{opts.PIFOMethod2} cost: {heuristicSolution.Cost}"); + Console.WriteLine($"Gap: {heuristicSolution.Cost - optimalSolution.Cost}"); + Console.WriteLine($"Inversions ({opts.PIFOMethod1}): {numInvOpt}"); + Console.WriteLine($"Inversions ({opts.PIFOMethod2}): {numInvHeu}"); + Console.WriteLine($"Time: {timer.ElapsedMilliseconds}ms"); + Console.WriteLine(new string('=', 60)); + + // Verbose output: full solution details + if (opts.Verbose) + { + Console.WriteLine($"\n{opts.PIFOMethod1} Solution:"); + Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject( + optimalSolution, Newtonsoft.Json.Formatting.Indented)); + Console.WriteLine($"\n{opts.PIFOMethod2} Solution:"); + Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject( + heuristicSolution, Newtonsoft.Json.Formatting.Indented)); + } + } + } +} \ No newline at end of file diff --git a/MetaOptimize.Cli/Program.cs b/MetaOptimize.Cli/Program.cs new file mode 100644 index 000000000..93daeb325 --- /dev/null +++ b/MetaOptimize.Cli/Program.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace MetaOptimize.Cli +{ + using CommandLine; + + /// + /// Entry point for MetaOptimize CLI. + /// Routes to different problem solvers based on command-line arguments. + /// + /// + /// Supports four problem types: + /// - TrafficEngineering: Find worst-case demand patterns for routing heuristics + /// - BinPacking: Find adversarial item sizes that maximize FFD vs optimal gap + /// - PIFO: Find packet sequences that maximize scheduling inversions + /// - FailureAnalysis: Analyze network resilience under link failures + /// + /// Uses CommandLineParser for argument parsing with CliOptions. + /// + public class Program + { + /// + /// Main entry point for the program. + /// Parses command-line arguments and dispatches to the appropriate runner. + /// + /// Command-line arguments. + public static void Main(string[] args) + { + var parseResult = CommandLine.Parser.Default.ParseArguments(args); + + parseResult.WithParsed(opts => + { + CliOptions.Instance = opts; + RunWithOptions(opts); + }); + + parseResult.WithNotParsed(errors => + { + // CommandLineParser prints help/errors automatically + Environment.Exit(1); + }); + } + + /// + /// Executes the appropriate runner based on the problem type. + /// + /// Parsed command-line options. + private static void RunWithOptions(CliOptions opts) + { + try + { + // Debug output + if (opts.Verbose) + { + Console.WriteLine($"[DEBUG] UseDefaultTopology: {opts.UseDefaultTopology}"); + Console.WriteLine($"[DEBUG] BreakSymmetry: {opts.BreakSymmetry}"); + Console.WriteLine($"[DEBUG] EnableClustering: {opts.EnableClustering}"); + Console.WriteLine($"[DEBUG] Verbose: {opts.Verbose}"); + Console.WriteLine($"[DEBUG] Debug: {opts.Debug}"); + } + + switch (opts.ProblemType) + { + case ProblemType.TrafficEngineering: + TERunner.Run(opts); + break; + + case ProblemType.BinPacking: + BPRunner.Run(opts); + break; + + case ProblemType.PIFO: + PIFORunner.Run(opts); + break; + + case ProblemType.FailureAnalysis: + FailureAnalysisRunner.Run(opts); + break; + + default: + Console.WriteLine($"ERROR: Unknown problem type '{opts.ProblemType}'"); + Environment.Exit(1); + break; + } + + Console.WriteLine(new string('=', 60)); + Console.WriteLine("Execution completed successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"\nERROR: {ex.Message}"); + Console.WriteLine($"Stack Trace:\n{ex.StackTrace}"); + Environment.Exit(1); + } + } + } +} \ No newline at end of file diff --git a/MetaOptimize.Cli/TERunner.cs b/MetaOptimize.Cli/TERunner.cs new file mode 100644 index 000000000..8ba9f87df --- /dev/null +++ b/MetaOptimize.Cli/TERunner.cs @@ -0,0 +1,352 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace MetaOptimize.Cli +{ + using System.Diagnostics; + using CommandLine; + using Gurobi; + using ZenLib; + using ZenLib.ModelChecking; + + /// + /// Runner for Traffic Engineering adversarial optimization. + /// Finds demand patterns that maximize the gap between optimal routing and heuristic algorithms. + /// + /// + /// Flow: Load topology → Configure heuristic → Run adversarial optimization → Validate results. + /// + /// Supports multiple heuristics: + /// - Pop: Partition-based routing with random demand partitioning + /// - DemandPinning: Threshold-based path selection + /// - ExpectedPop: Average performance across multiple partitions + /// - PopDp: Combined Pop and DemandPinning + /// + /// Supports multiple search methods: + /// - Direct: Find maximum gap directly via bilevel optimization + /// - Search: Binary search for gap within interval + /// - FindFeas: Find any solution with gap >= threshold + /// - Random: Random sampling for gap estimation + /// - HillClimber: Local search with neighborhood exploration + /// - SimulatedAnnealing: Probabilistic local search with temperature cooling + /// + /// Includes clustering support for scalable optimization on large topologies. + /// + public static class TERunner + { + /// + /// Runs Traffic Engineering adversarial optimization. + /// Main entry point that loads topology and dispatches to solver. + /// + /// Command-line options containing TE parameters. + public static void Run(CliOptions opts) + { + if (opts == null) + { + Console.WriteLine("ERROR: Options not parsed correctly."); + Environment.Exit(1); + } + + // Load topology and optional cluster configurations + var (topology, clusters) = CliUtils.getTopology( + opts.TopologyFile, + opts.PathFile, + opts.DownScaleFactor, + opts.EnableClustering, + opts.NumClusters, + opts.ClusterDir, + opts.Verbose); + + GetSolverAndRunNetwork(topology, clusters); + } + + /// + /// Creates the appropriate solver and runs network optimization. + /// Dispatches to Zen (SMT) or Gurobi (MIP) based on configuration. + /// + /// The network topology to optimize. + /// Optional cluster topologies for hierarchical optimization. + /// Thrown when an unsupported solver is specified. + private static void GetSolverAndRunNetwork(Topology topology, List clusters) + { + var opts = CliOptions.Instance; + + switch (opts.SolverChoice) + { + case SolverChoice.OrTools: + RunNetwork(new ORToolsSolver(), topology, clusters); + break; + + case SolverChoice.Zen: + RunNetwork(new SolverZen(), topology, clusters); + break; + + case SolverChoice.Gurobi: + var storeProgress = opts.StoreProgress && (opts.Method == MethodChoice.Direct); + var solver = new GurobiSOS( + opts.Timeout, + Convert.ToInt32(opts.Verbose), + timeToTerminateNoImprovement: opts.TimeToTerminateIfNoImprovement, + numThreads: opts.NumGurobiThreads, + recordProgress: storeProgress, + logPath: opts.LogFile); + RunNetwork(solver, topology, clusters); + break; + + default: + throw new Exception($"Unsupported solver: {opts.SolverChoice}. Valid options: OrTools, Gurobi, Zen"); + } + } + + /// + /// Generic implementation of traffic engineering adversarial optimization. + /// + /// Solver variable type (GRBVar or Zen). + /// Solver solution type (GRBModel or ZenSolution). + /// The solver instance to use. + /// The network topology to optimize. + /// Optional cluster topologies for hierarchical optimization. + /// + /// Execution phases: + /// 1. Setup: Create optimal encoder, heuristic encoder, and adversarial generator + /// 2. Optimization: Run selected method (Direct, Search, Random, etc.) + /// 3. Post-processing: Optional FullOpt and UBFocus refinement + /// 4. Validation: Verify solution with independent solver instances. + /// + private static void RunNetwork( + ISolver solver, + Topology topology, + List clusters) + { + var opts = CliOptions.Instance; + + // Setup optimal encoder for maximum flow + var optimalEncoder = new TEMaxFlowOptimalEncoder(solver, opts.Paths); + + // Setup adversarial input generator + var adversarialInputGenerator = new TEAdversarialInputGenerator( + topology, opts.Paths, opts.NumProcesses); + + // Setup heuristic encoder based on selected algorithm + var (heuristicEncoder, partitioning, partitionList) = CliUtils.getHeuristic( + solver, + topology, + opts.Heuristic, + opts.Paths, + opts.PopSlices, + opts.DemandPinningThreshold * opts.DownScaleFactor, + numSamples: opts.NumRandom, + partitionSensitivity: opts.PartitionSensitivity, + scaleFactor: opts.DownScaleFactor, + InnerEncoding: opts.InnerEncoding, + maxShortestPathLen: opts.MaxShortestPathLen); + + // Parse demand quantization levels + var demandList = new GenericList( + opts.DemandList.Split(",") + .Select(x => double.Parse(x) * opts.DownScaleFactor) + .ToHashSet()); + + Utils.logger( + $"Demand List:{Newtonsoft.Json.JsonConvert.SerializeObject(demandList.List, Newtonsoft.Json.Formatting.Indented)}", + opts.Verbose); + + var timer = Stopwatch.StartNew(); + Utils.logger("Starting optimization", opts.Verbose); + + // Run selected optimization method + (TEOptimizationSolution, TEOptimizationSolution) result; + switch (opts.Method) + { + case MethodChoice.Direct: + result = CliUtils.getMetaOptResult( + adversarialInputGenerator, optimalEncoder, heuristicEncoder, + opts.DemandUB, opts.InnerEncoding, demandList, + opts.EnableClustering, opts.ClusterVersion, clusters, + opts.NumInterClusterSamples, opts.NumNodesPerCluster, + opts.NumInterClusterQuantizations, opts.Simplify, opts.Verbose, + opts.MaxDensity, opts.LargeDemandLB, opts.maxLargeDistance, + opts.maxSmallDistance, false, null); + break; + + case MethodChoice.Search: + Utils.logger("Using interval search for gap", opts.Verbose); + result = adversarialInputGenerator.FindMaximumGapInterval( + optimalEncoder, heuristicEncoder, + opts.Confidencelvl, opts.StartingGap, opts.DemandUB, + demandList: demandList); + break; + + case MethodChoice.FindFeas: + Utils.logger("Finding feasible solution with target gap", opts.Verbose); + result = adversarialInputGenerator.FindOptimalityGapAtLeast( + optimalEncoder, heuristicEncoder, + opts.StartingGap, opts.DemandUB, + demandList: demandList, simplify: opts.Simplify); + break; + + case MethodChoice.Random: + Utils.logger("Using random search", opts.Verbose); + result = adversarialInputGenerator.RandomAdversarialGenerator( + optimalEncoder, heuristicEncoder, + opts.NumRandom, opts.DemandUB, + seed: opts.Seed, verbose: opts.Verbose, + storeProgress: opts.StoreProgress, logPath: opts.LogFile, + timeout: opts.Timeout); + break; + + case MethodChoice.HillClimber: + Utils.logger("Using hill climbing", opts.Verbose); + result = adversarialInputGenerator.HillClimbingAdversarialGenerator( + optimalEncoder, heuristicEncoder, + opts.NumRandom, opts.NumNeighbors, opts.DemandUB, opts.StdDev, + seed: opts.Seed, verbose: opts.Verbose, + storeProgress: opts.StoreProgress, logPath: opts.LogFile, + timeout: opts.Timeout); + break; + + case MethodChoice.SimulatedAnnealing: + Utils.logger("Using simulated annealing", opts.Verbose); + result = adversarialInputGenerator.SimulatedAnnealing( + optimalEncoder, heuristicEncoder, + opts.NumRandom, opts.NumNeighbors, opts.DemandUB, opts.StdDev, + opts.InitTmp, opts.TmpDecreaseFactor, + seed: opts.Seed, verbose: opts.Verbose, + storeProgress: opts.StoreProgress, logPath: opts.LogFile, + timeout: opts.Timeout); + break; + + default: + throw new Exception($"Unknown method: {opts.Method}. " + + "Valid options: Direct, Search, FindFeas, Random, HillClimber, SimulatedAnnealing"); + } + + // Post-processing: Full optimization refinement (clustering only) + if (opts.FullOpt) + { + if (!opts.EnableClustering) + { + throw new Exception("FullOpt requires clustering to be enabled"); + } + if (opts.InnerEncoding != InnerRewriteMethodChoice.PrimalDual) + { + throw new Exception("FullOpt requires PrimalDual inner encoding"); + } + + optimalEncoder.Solver.CleanAll(timeout: opts.FullOptTimer); + var currDemands = new Dictionary<(string, string), double>(result.Item1.Demands); + Utils.setEmptyPairsToZero(topology, currDemands); + + result = adversarialInputGenerator.MaximizeOptimalityGap( + optimalEncoder, heuristicEncoder, opts.DemandUB, + innerEncoding: opts.InnerEncoding, demandList: demandList, + simplify: opts.Simplify, verbose: opts.Verbose, + demandInits: currDemands); + + optimalEncoder.Solver.CleanAll(focusBstBd: false, timeout: opts.Timeout); + } + + // Post-processing: Upper bound focus refinement + if (opts.UBFocus) + { + var currDemands = new Dictionary<(string, string), double>(result.Item1.Demands); + optimalEncoder.Solver.CleanAll(focusBstBd: true, timeout: opts.UBFocusTimer); + Utils.setEmptyPairsToZero(topology, currDemands); + + result = adversarialInputGenerator.MaximizeOptimalityGap( + optimalEncoder, heuristicEncoder, opts.DemandUB, + innerEncoding: opts.InnerEncoding, demandList: demandList, + simplify: opts.Simplify, verbose: opts.Verbose, + demandInits: currDemands); + + optimalEncoder.Solver.CleanAll(focusBstBd: false, timeout: opts.Timeout); + } + + timer.Stop(); + + // Extract results + var optimal = result.Item1.MaxObjective; + var heuristic = result.Item2.MaxObjective; + var demands = new Dictionary<(string, string), double>(result.Item1.Demands); + Utils.setEmptyPairsToZero(topology, demands); + + // Display results + Console.WriteLine("##############################################"); + Console.WriteLine("RESULTS:"); + Console.WriteLine($"Optimal: {optimal}"); + Console.WriteLine($"Heuristic: {heuristic}"); + Console.WriteLine($"Gap: {optimal - heuristic}"); + Console.WriteLine($"Time: {timer.ElapsedMilliseconds}ms"); + Console.WriteLine("##############################################"); + + // Special handling for ExpectedPop heuristic + if (opts.Heuristic == Heuristic.ExpectedPop) + { + CliUtils.findGapExpectedPopAdversarialDemandOnIndependentPartitions( + opts, topology, demands, optimal); + } + + // Validation: verify solution with independent solvers + ValidateSolution(opts, topology, partitioning, partitionList, optimal, heuristic, demands); + } + + /// + /// Validates the solution using independent Gurobi solver instances. + /// + private static void ValidateSolution( + CliOptions opts, + Topology topology, + IDictionary<(string, string), int> partitioning, + IList> partitionList, + double optimal, + double heuristic, + Dictionary<(string, string), double> demands) + { + Console.WriteLine("Validating solution..."); + + var optGSolver = new GurobiBinary(); + var optimalEncoderG = new TEMaxFlowOptimalEncoder( + optGSolver, maxNumPaths: opts.Paths); + + var gSolver = new GurobiBinary(); + IEncoder heuristicEncoderG; + + switch (opts.Heuristic) + { + case Heuristic.Pop: + heuristicEncoderG = new PopEncoder( + gSolver, maxNumPaths: opts.Paths, + numPartitions: opts.PopSlices, + demandPartitions: partitioning); + break; + + case Heuristic.DemandPinning: + heuristicEncoderG = new DirectDemandPinningEncoder( + gSolver, k: opts.Paths, + threshold: opts.DemandPinningThreshold * opts.DownScaleFactor); + break; + + case Heuristic.ExpectedPop: + heuristicEncoderG = new ExpectedPopEncoder( + gSolver, k: opts.Paths, + numSamples: opts.NumRandom, + numPartitionsPerSample: opts.PopSlices, + demandPartitionsList: partitionList); + break; + + case Heuristic.PopDp: + throw new Exception("PopDp validation not implemented yet."); + + default: + throw new Exception($"Unknown heuristic for validation: {opts.Heuristic}"); + } + + Utils.checkSolution( + topology, heuristicEncoderG, optimalEncoderG, + heuristic, optimal, demands, "gurobiCheck"); + + Console.WriteLine("Validation completed."); + } + } +} \ No newline at end of file diff --git a/MetaOptimize.Cli/cliUtils.cs b/MetaOptimize.Cli/cliUtils.cs index 9d45e254a..79333f527 100644 --- a/MetaOptimize.Cli/cliUtils.cs +++ b/MetaOptimize.Cli/cliUtils.cs @@ -219,7 +219,8 @@ public static (IEncoder, IDictionary<(string, string), int>, IL public static (Topology, List) getTopology(string topologyFile, string pathFile, double downScaleFactor, bool enableClustering, int numClusters, string clusterDir, bool verbose) { - Topology topology = Parser.ReadTopologyJson(topologyFile, pathFile, scaleFactor: downScaleFactor); + string topoPath = ResolveTopologyPath(topologyFile); + Topology topology = Parser.ReadTopologyJson(topoPath, pathFile, scaleFactor: downScaleFactor); List clusters = new List(); if (enableClustering) { @@ -233,6 +234,41 @@ public static (Topology, List) getTopology(string topologyFile, string return (topology, clusters); } + /// + /// It makes sure the topology file path works for both VSCode and CLI run modes. + /// + /// + /// Valid topology path. + private static string ResolveTopologyPath(string topologyFile) + { + // If absolute path or file exists, use as-is + if (Path.IsPathRooted(topologyFile) || File.Exists(topologyFile)) + { + return topologyFile; + } + + // Try common locations + var candidates = new[] + { + topologyFile, + Path.Combine("Topologies", Path.GetFileName(topologyFile)), + Path.Combine("..", "Topologies", Path.GetFileName(topologyFile)), + Path.Combine("..", "MetaOpt", "Topologies", Path.GetFileName(topologyFile)), + }; + + foreach (var candidate in candidates) + { + if (File.Exists(candidate)) + { + Console.WriteLine($"Found topology at: {Path.GetFullPath(candidate)}"); + return candidate; + } + } + + // Fall back to original (will fail with clear error) + return topologyFile; + } + /// /// Get the method from MetaOpt to find adversarial inputs. /// diff --git a/MetaOptimize.Test/CliOptionsTests.cs b/MetaOptimize.Test/CliOptionsTests.cs new file mode 100644 index 000000000..033c1929d --- /dev/null +++ b/MetaOptimize.Test/CliOptionsTests.cs @@ -0,0 +1,667 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace MetaOptimize.Test +{ + using System.Collections.Generic; + using CommandLine; + using MetaOptimize.Cli; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Tests for CLI options parsing and runner integration. + /// Validates the unified CliOptions system works for all problem types. + /// + [TestClass] + public class CliOptionsTests + { + #region CliOptions Parsing Tests + + /// + /// Test that default values are set correctly when no arguments provided. + /// + [TestMethod] + public void TestCliOptionsDefaults() + { + var args = new string[] { }; + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + // Common defaults + Assert.AreEqual(ProblemType.TrafficEngineering, opts.ProblemType); + Assert.AreEqual(SolverChoice.Gurobi, opts.SolverChoice); + Assert.AreEqual(false, opts.Verbose); + Assert.AreEqual(false, opts.Debug); + + // BinPacking defaults + Assert.AreEqual(6, opts.NumBins); + Assert.AreEqual(9, opts.NumDemands); + Assert.AreEqual(2, opts.NumDimensions); + Assert.AreEqual(3, opts.OptimalBins); + Assert.AreEqual(FFDMethodChoice.FFDSum, opts.FFMethod); + Assert.AreEqual(false, opts.BreakSymmetry); + + // PIFO defaults + Assert.AreEqual(18, opts.NumPackets); + Assert.AreEqual(8, opts.MaxRank); + Assert.AreEqual(4, opts.NumQueues); + Assert.AreEqual(12, opts.MaxQueueSize); + Assert.AreEqual(12, opts.WindowSize); + Assert.AreEqual(0.1, opts.BurstParam); + + // FailureAnalysis defaults + Assert.AreEqual(true, opts.UseDefaultTopology); + Assert.AreEqual(1, opts.MaxNumFailures); + Assert.AreEqual(1, opts.NumExtraPaths); + Assert.AreEqual(0.25, opts.FailureProbThreshold); + + // TE defaults + Assert.AreEqual(Heuristic.Pop, opts.Heuristic); + Assert.AreEqual(2, opts.Paths); + Assert.AreEqual(2, opts.PopSlices); + Assert.AreEqual(MethodChoice.Direct, opts.Method); + Assert.AreEqual(InnerRewriteMethodChoice.KKT, opts.InnerEncoding); + }); + } + + /// + /// Test parsing BinPacking problem type with custom parameters. + /// + [TestMethod] + public void TestCliOptionsBinPackingParsing() + { + var args = new string[] + { + "--problemType", "BinPacking", + "--solver", "Gurobi", + "--numBins", "10", + "--numDemands", "15", + "--numDimensions", "3", + "--optimalBins", "5", + "--ffMethod", "FFDProd", + "--breakSymmetry", "true", + "--binCapacity", "1.5,1.5,1.5", + "--verbose", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.BinPacking, opts.ProblemType); + Assert.AreEqual(SolverChoice.Gurobi, opts.SolverChoice); + Assert.AreEqual(10, opts.NumBins); + Assert.AreEqual(15, opts.NumDemands); + Assert.AreEqual(3, opts.NumDimensions); + Assert.AreEqual(5, opts.OptimalBins); + Assert.AreEqual(FFDMethodChoice.FFDProd, opts.FFMethod); + Assert.AreEqual(true, opts.BreakSymmetry); + Assert.AreEqual("1.5,1.5,1.5", opts.BinCapacity); + Assert.AreEqual(true, opts.Verbose); + }); + + result.WithNotParsed(errors => + { + Assert.Fail("Failed to parse BinPacking arguments"); + }); + } + + /// + /// Test parsing PIFO problem type with custom parameters. + /// + [TestMethod] + public void TestCliOptionsPIFOParsing() + { + var args = new string[] + { + "--problemType", "PIFO", + "--solver", "Gurobi", + "--numPackets", "24", + "--maxRank", "10", + "--numQueues", "6", + "--maxQueueSize", "15", + "--windowSize", "15", + "--burstParam", "0.2", + "--timeout", "500", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.PIFO, opts.ProblemType); + Assert.AreEqual(24, opts.NumPackets); + Assert.AreEqual(10, opts.MaxRank); + Assert.AreEqual(6, opts.NumQueues); + Assert.AreEqual(15, opts.MaxQueueSize); + Assert.AreEqual(15, opts.WindowSize); + Assert.AreEqual(0.2, opts.BurstParam); + Assert.AreEqual(500, opts.Timeout); + }); + + result.WithNotParsed(errors => + { + Assert.Fail("Failed to parse PIFO arguments"); + }); + } + + /// + /// Test parsing FailureAnalysis problem type with custom parameters. + /// + [TestMethod] + public void TestCliOptionsFailureAnalysisParsing() + { + var args = new string[] + { + "--problemType", "FailureAnalysis", + "--solver", "Gurobi", + "--useDefaultTopology", "true", + "--maxNumFailures", "2", + "--numExtraPaths", "2", + "--failureProbThreshold", "0.1", + "--innerencoding", "PrimalDual", + "--demandlist", "0,5,10,15", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.FailureAnalysis, opts.ProblemType); + Assert.AreEqual(true, opts.UseDefaultTopology); + Assert.AreEqual(2, opts.MaxNumFailures); + Assert.AreEqual(2, opts.NumExtraPaths); + Assert.AreEqual(0.1, opts.FailureProbThreshold); + Assert.AreEqual(InnerRewriteMethodChoice.PrimalDual, opts.InnerEncoding); + Assert.AreEqual("0,5,10,15", opts.DemandList); + }); + + result.WithNotParsed(errors => + { + Assert.Fail("Failed to parse FailureAnalysis arguments"); + }); + } + + /// + /// Test parsing TrafficEngineering problem type with custom parameters. + /// + [TestMethod] + public void TestCliOptionsTrafficEngineeringParsing() + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--topologyFile", "Swan.json", + "--heuristic", "DemandPinning", + "--solver", "Gurobi", + "--paths", "3", + "--pinthreshold", "0.7", + "--method", "Direct", + "--innerencoding", "KKT", + "--verbose", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.TrafficEngineering, opts.ProblemType); + Assert.AreEqual("Swan.json", opts.TopologyFile); + Assert.AreEqual(Heuristic.DemandPinning, opts.Heuristic); + Assert.AreEqual(SolverChoice.Gurobi, opts.SolverChoice); + Assert.AreEqual(3, opts.Paths); + Assert.AreEqual(0.7, opts.DemandPinningThreshold); + Assert.AreEqual(MethodChoice.Direct, opts.Method); + Assert.AreEqual(InnerRewriteMethodChoice.KKT, opts.InnerEncoding); + Assert.AreEqual(true, opts.Verbose); + }); + + result.WithNotParsed(errors => + { + Assert.Fail("Failed to parse TrafficEngineering arguments"); + }); + } + + /// + /// Test all FFDMethodChoice values parse correctly. + /// + [TestMethod] + public void TestCliOptionsFFMethodChoices() + { + var methods = new[] { "FF", "FFDSum", "FFDProd", "FFDDiv" }; + var expected = new[] { FFDMethodChoice.FF, FFDMethodChoice.FFDSum, FFDMethodChoice.FFDProd, FFDMethodChoice.FFDDiv }; + + for (int i = 0; i < methods.Length; i++) + { + var args = new string[] { "--problemType", "BinPacking", "--ffMethod", methods[i] }; + var result = CommandLine.Parser.Default.ParseArguments(args); + + var expectedMethod = expected[i]; + result.WithParsed(opts => + { + Assert.AreEqual(expectedMethod, opts.FFMethod, $"Failed for {methods[i]}"); + }); + } + } + + /// + /// Test all SolverChoice values parse correctly. + /// + [TestMethod] + public void TestCliOptionsSolverChoices() + { + var solvers = new[] { "Gurobi", "Zen" }; + var expected = new[] { SolverChoice.Gurobi, SolverChoice.Zen }; + + for (int i = 0; i < solvers.Length; i++) + { + var args = new string[] { "--solver", solvers[i] }; + var result = CommandLine.Parser.Default.ParseArguments(args); + + var expectedSolver = expected[i]; + result.WithParsed(opts => + { + Assert.AreEqual(expectedSolver, opts.SolverChoice, $"Failed for {solvers[i]}"); + }); + } + } + + /// + /// Test all MethodChoice values parse correctly. + /// + [TestMethod] + public void TestCliOptionsMethodChoices() + { + var methods = new[] { "Direct", "Search", "FindFeas", "Random", "HillClimber", "SimulatedAnnealing" }; + var expected = new[] + { + MethodChoice.Direct, MethodChoice.Search, MethodChoice.FindFeas, + MethodChoice.Random, MethodChoice.HillClimber, MethodChoice.SimulatedAnnealing, + }; + + for (int i = 0; i < methods.Length; i++) + { + var args = new string[] { "--method", methods[i] }; + var result = CommandLine.Parser.Default.ParseArguments(args); + + var expectedMethod = expected[i]; + result.WithParsed(opts => + { + Assert.AreEqual(expectedMethod, opts.Method, $"Failed for {methods[i]}"); + }); + } + } + + /// + /// Test all Heuristic values parse correctly. + /// + [TestMethod] + public void TestCliOptionsHeuristicChoices() + { + var heuristics = new[] { "Pop", "DemandPinning", "ExpectedPop", "PopDp", "ModifiedDp" }; + var expected = new[] + { + Heuristic.Pop, Heuristic.DemandPinning, Heuristic.ExpectedPop, + Heuristic.PopDp, Heuristic.ModifiedDp, + }; + + for (int i = 0; i < heuristics.Length; i++) + { + var args = new string[] { "--heuristic", heuristics[i] }; + var result = CommandLine.Parser.Default.ParseArguments(args); + + var expectedHeuristic = expected[i]; + result.WithParsed(opts => + { + Assert.AreEqual(expectedHeuristic, opts.Heuristic, $"Failed for {heuristics[i]}"); + }); + } + } + + #endregion + + #region BinPacking Integration Tests + + /// + /// Test BinPacking runner executes with CliOptions. + /// Uses small parameters for fast execution. + /// + [TestMethod] + public void TestBinPackingRunnerSmall() + { + var args = new string[] + { + "--problemType", "BinPacking", + "--solver", "Gurobi", + "--numBins", "4", + "--numDemands", "6", + "--numDimensions", "2", + "--optimalBins", "2", + "--ffMethod", "FFDSum", + "--timeout", "60", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + CliOptions.Instance = opts; + + // Verify options are set correctly + Assert.AreEqual(ProblemType.BinPacking, opts.ProblemType); + Assert.AreEqual(4, opts.NumBins); + Assert.AreEqual(6, opts.NumDemands); + Assert.AreEqual(2, opts.NumDimensions); + Assert.AreEqual(2, opts.OptimalBins); + + // Parse bin capacities (same logic as BPRunner) + var binCapacities = opts.BinCapacity.Split(',').Select(double.Parse).ToList(); + while (binCapacities.Count < opts.NumDimensions) + { + binCapacities.Add(1.00001); + } + + Assert.AreEqual(2, binCapacities.Count); + + // Create components (don't run full optimization - too slow for unit test) + var solver = new GurobiSOS(timeout: opts.Timeout, verbose: 0); + var bins = new Bins(opts.NumBins, binCapacities); + + Assert.IsNotNull(solver); + Assert.IsNotNull(bins); + }); + } + + /// + /// Test BinPacking bin capacity parsing handles various formats. + /// + [TestMethod] + public void TestBinPackingCapacityParsing() + { + // Single dimension + var capacities1 = "1.5".Split(',').Select(double.Parse).ToList(); + Assert.AreEqual(1, capacities1.Count); + Assert.AreEqual(1.5, capacities1[0]); + + // Two dimensions + var capacities2 = "1.00001,1.00001".Split(',').Select(double.Parse).ToList(); + Assert.AreEqual(2, capacities2.Count); + Assert.AreEqual(1.00001, capacities2[0]); + Assert.AreEqual(1.00001, capacities2[1]); + + // Three dimensions + var capacities3 = "2.0,1.5,1.0".Split(',').Select(double.Parse).ToList(); + Assert.AreEqual(3, capacities3.Count); + Assert.AreEqual(2.0, capacities3[0]); + Assert.AreEqual(1.5, capacities3[1]); + Assert.AreEqual(1.0, capacities3[2]); + } + + #endregion + + #region PIFO Integration Tests + + /// + /// Test PIFO runner options are parsed correctly. + /// + [TestMethod] + public void TestPIFORunnerOptions() + { + var args = new string[] + { + "--problemType", "PIFO", + "--solver", "Gurobi", + "--numPackets", "12", + "--maxRank", "6", + "--numQueues", "3", + "--maxQueueSize", "8", + "--windowSize", "8", + "--burstParam", "0.15", + "--timeout", "30", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + CliOptions.Instance = opts; + + Assert.AreEqual(ProblemType.PIFO, opts.ProblemType); + Assert.AreEqual(12, opts.NumPackets); + Assert.AreEqual(6, opts.MaxRank); + Assert.AreEqual(3, opts.NumQueues); + Assert.AreEqual(8, opts.MaxQueueSize); + Assert.AreEqual(8, opts.WindowSize); + Assert.AreEqual(0.15, opts.BurstParam); + Assert.AreEqual(30, opts.Timeout); + + // Create solver (don't run full optimization) + var solver = new GurobiSOS(timeout: opts.Timeout, verbose: 0); + Assert.IsNotNull(solver); + }); + } + + #endregion + + #region FailureAnalysis Integration Tests + + /// + /// Test FailureAnalysis runner options are parsed correctly. + /// + [TestMethod] + public void TestFailureAnalysisRunnerOptions() + { + var args = new string[] + { + "--problemType", "FailureAnalysis", + "--solver", "Gurobi", + "--useDefaultTopology", "true", + "--maxNumFailures", "1", + "--numExtraPaths", "1", + "--failureProbThreshold", "0.2", + "--timeout", "30", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + CliOptions.Instance = opts; + + Assert.AreEqual(ProblemType.FailureAnalysis, opts.ProblemType); + Assert.AreEqual(true, opts.UseDefaultTopology); + Assert.AreEqual(1, opts.MaxNumFailures); + Assert.AreEqual(1, opts.NumExtraPaths); + Assert.AreEqual(0.2, opts.FailureProbThreshold); + + // Parse demand list (same logic as FailureAnalysisRunner) + var demandSet = new HashSet(opts.DemandList.Split(',').Select(double.Parse)); + Assert.IsTrue(demandSet.Count > 0); + }); + } + + /// + /// Test demand list parsing for FailureAnalysis. + /// + [TestMethod] + public void TestFailureAnalysisDemandListParsing() + { + var args = new string[] + { + "--problemType", "FailureAnalysis", + "--demandlist", "0,5,10,15,20", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + var demandSet = new HashSet(opts.DemandList.Split(',').Select(double.Parse)); + Assert.AreEqual(5, demandSet.Count); + Assert.IsTrue(demandSet.Contains(0)); + Assert.IsTrue(demandSet.Contains(5)); + Assert.IsTrue(demandSet.Contains(10)); + Assert.IsTrue(demandSet.Contains(15)); + Assert.IsTrue(demandSet.Contains(20)); + }); + } + + #endregion + + #region TrafficEngineering Integration Tests + + /// + /// Test TrafficEngineering runner options are parsed correctly. + /// + [TestMethod] + public void TestTrafficEngineeringRunnerOptions() + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--topologyFile", "Topologies/simple.json", + "--heuristic", "Pop", + "--solver", "Gurobi", + "--paths", "2", + "--slices", "2", + "--method", "Direct", + "--timeout", "30", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + CliOptions.Instance = opts; + + Assert.AreEqual(ProblemType.TrafficEngineering, opts.ProblemType); + Assert.AreEqual("Topologies/simple.json", opts.TopologyFile); + Assert.AreEqual(Heuristic.Pop, opts.Heuristic); + Assert.AreEqual(2, opts.Paths); + Assert.AreEqual(2, opts.PopSlices); + Assert.AreEqual(MethodChoice.Direct, opts.Method); + }); + } + + /// + /// Test TrafficEngineering DemandPinning options. + /// + [TestMethod] + public void TestTrafficEngineeringDemandPinningOptions() + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--heuristic", "DemandPinning", + "--pinthreshold", "0.6", + "--paths", "3", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(Heuristic.DemandPinning, opts.Heuristic); + Assert.AreEqual(0.6, opts.DemandPinningThreshold); + Assert.AreEqual(3, opts.Paths); + }); + } + + /// + /// Test TrafficEngineering PrimalDual encoding options. + /// + [TestMethod] + public void TestTrafficEngineeringPrimalDualOptions() + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--innerencoding", "PrimalDual", + "--demandlist", "0,10,20,30", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(InnerRewriteMethodChoice.PrimalDual, opts.InnerEncoding); + Assert.AreEqual("0,10,20,30", opts.DemandList); + + // Parse and validate + var demandSet = new HashSet(opts.DemandList.Split(',').Select(double.Parse)); + Assert.AreEqual(4, demandSet.Count); + }); + } + + #endregion + + #region Edge Cases and Error Handling + + /// + /// Test that CliOptions.Instance is set correctly. + /// + [TestMethod] + public void TestCliOptionsInstance() + { + var args = new string[] { "--problemType", "BinPacking" }; + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + CliOptions.Instance = opts; + Assert.IsNotNull(CliOptions.Instance); + Assert.AreEqual(ProblemType.BinPacking, CliOptions.Instance.ProblemType); + }); + } + + /// + /// Test short option flags work. + /// + [TestMethod] + public void TestShortOptionFlags() + { + var args = new string[] + { + "-f", "test.json", + "-h", "Pop", + "-c", "Gurobi", + "-v", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual("test.json", opts.TopologyFile); + Assert.AreEqual(Heuristic.Pop, opts.Heuristic); + Assert.AreEqual(SolverChoice.Gurobi, opts.SolverChoice); + Assert.AreEqual(true, opts.Verbose); + }); + } + + /// + /// Test timeout parsing with various values. + /// + [TestMethod] + public void TestTimeoutParsing() + { + // Finite timeout + var args1 = new string[] { "--timeout", "100" }; + CommandLine.Parser.Default.ParseArguments(args1).WithParsed(opts => + { + Assert.AreEqual(100, opts.Timeout); + }); + + // Large timeout + var args2 = new string[] { "--timeout", "3600" }; + CommandLine.Parser.Default.ParseArguments(args2).WithParsed(opts => + { + Assert.AreEqual(3600, opts.Timeout); + }); + } + + #endregion + } +} \ No newline at end of file diff --git a/MetaOptimize.Test/CliParameterCombinationTests.cs b/MetaOptimize.Test/CliParameterCombinationTests.cs new file mode 100644 index 000000000..929561efb --- /dev/null +++ b/MetaOptimize.Test/CliParameterCombinationTests.cs @@ -0,0 +1,1455 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// + +namespace MetaOptimize.Test +{ + using System; + using System.Linq; + using CommandLine; + using Gurobi; + using MetaOptimize.Cli; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Comprehensive tests for CLI parameter combinations. + /// Validates that all parameter combinations parse correctly and runners can be initialized. + /// + [TestClass] + public class CliParameterCombinationTests + { + #region Problem Type x Solver Combinations + + /// + /// Test all ProblemType and SolverChoice combinations parse correctly. + /// + [TestMethod] + public void TestAllProblemTypeSolverCombinations() + { + var problemTypes = new[] { "TrafficEngineering", "BinPacking", "PIFO", "FailureAnalysis" }; + var solvers = new[] { "Gurobi", "Zen" }; + + foreach (var problemType in problemTypes) + { + foreach (var solver in solvers) + { + var args = new string[] { "--problemType", problemType, "--solver", solver }; + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.IsNotNull(opts, $"Failed to parse: {problemType} + {solver}"); + }); + + result.WithNotParsed(errors => + { + Assert.Fail($"Parse failed for {problemType} + {solver}"); + }); + } + } + } + + #endregion + + #region Traffic Engineering - Heuristic Combinations + + /// + /// Test all TE heuristic types parse correctly. + /// + [TestMethod] + public void TestTEAllHeuristics() + { + var heuristics = new[] + { + "Pop", "DemandPinning", "ExpectedPop", "PopDp", + "ExpectedPopDp", "ParallelPop", "ParallelPopDp", "ModifiedDp", + }; + + foreach (var heuristic in heuristics) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--heuristic", heuristic, + "--solver", "Gurobi", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.TrafficEngineering, opts.ProblemType); + Assert.AreEqual(heuristic, opts.Heuristic.ToString(), $"Heuristic mismatch for {heuristic}"); + }); + + result.WithNotParsed(errors => + { + Assert.Fail($"Parse failed for heuristic: {heuristic}"); + }); + } + } + + /// + /// Test Pop heuristic with various slice counts. + /// + [TestMethod] + public void TestTEPopWithDifferentSlices() + { + var sliceCounts = new[] { 1, 2, 3, 4, 5, 10 }; + + foreach (var slices in sliceCounts) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--heuristic", "Pop", + "--slices", slices.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(slices, opts.PopSlices, $"Slices mismatch for {slices}"); + }); + } + } + + /// + /// Test DemandPinning with various thresholds. + /// + [TestMethod] + public void TestTEDemandPinningThresholds() + { + var thresholds = new[] { 0.1, 0.25, 0.5, 0.75, 1.0, 5.0, 10.0 }; + + foreach (var threshold in thresholds) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--heuristic", "DemandPinning", + "--pinthreshold", threshold.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(threshold, opts.DemandPinningThreshold, $"Threshold mismatch for {threshold}"); + }); + } + } + + /// + /// Test ModifiedDp with various max shortest path lengths. + /// + [TestMethod] + public void TestTEModifiedDpMaxPathLengths() + { + var lengths = new[] { -1, 1, 2, 3, 5, 10 }; + + foreach (var length in lengths) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--heuristic", "ModifiedDp", + "--maxshortestlen", length.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(length, opts.MaxShortestPathLen, $"MaxShortestPathLen mismatch for {length}"); + }); + } + } + + #endregion + + #region Traffic Engineering - Method Combinations + + /// + /// Test all TE method choices. + /// + [TestMethod] + public void TestTEAllMethods() + { + var methods = new[] + { + "Direct", "Search", "FindFeas", "Random", "HillClimber", "SimulatedAnnealing", + }; + + foreach (var method in methods) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--method", method, + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(method, opts.Method.ToString(), $"Method mismatch for {method}"); + }); + } + } + + /// + /// Test Search method with various confidence levels. + /// + [TestMethod] + public void TestTESearchMethodConfidenceLevels() + { + var confidences = new[] { 0.01, 0.05, 0.1, 0.2, 0.5 }; + + foreach (var confidence in confidences) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--method", "Search", + "--confidence", confidence.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(confidence, opts.Confidencelvl, $"Confidence mismatch for {confidence}"); + }); + } + } + + /// + /// Test Search/FindFeas methods with various starting gaps. + /// + [TestMethod] + public void TestTEStartingGaps() + { + var gaps = new[] { 1.0, 5.0, 10.0, 20.0, 50.0, 100.0 }; + + foreach (var gap in gaps) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--method", "FindFeas", + "--startinggap", gap.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(gap, opts.StartingGap, $"StartingGap mismatch for {gap}"); + }); + } + } + + /// + /// Test Random method with various trial counts. + /// + [TestMethod] + public void TestTERandomMethodTrials() + { + var trials = new[] { 1, 5, 10, 50, 100 }; + + foreach (var trial in trials) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--method", "Random", + "--num", trial.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(trial, opts.NumRandom, $"NumRandom mismatch for {trial}"); + }); + } + } + + /// + /// Test HillClimber with various neighbor counts. + /// + [TestMethod] + public void TestTEHillClimberNeighbors() + { + var neighbors = new[] { 1, 2, 5, 10, 20 }; + + foreach (var neighbor in neighbors) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--method", "HillClimber", + "--neighbors", neighbor.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(neighbor, opts.NumNeighbors, $"NumNeighbors mismatch for {neighbor}"); + }); + } + } + + /// + /// Test SimulatedAnnealing temperature parameters. + /// + [TestMethod] + public void TestTESimulatedAnnealingParams() + { + var initTemps = new[] { 0.5, 1.0, 2.0, 10.0 }; + var lambdas = new[] { 0.5, 0.9, 0.95, 0.99 }; + + foreach (var temp in initTemps) + { + foreach (var lambda in lambdas) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--method", "SimulatedAnnealing", + "--inittmp", temp.ToString(), + "--lambda", lambda.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(temp, opts.InitTmp, $"InitTmp mismatch for {temp}"); + Assert.AreEqual(lambda, opts.TmpDecreaseFactor, $"Lambda mismatch for {lambda}"); + }); + } + } + } + + #endregion + + #region Traffic Engineering - Inner Encoding + + /// + /// Test KKT and PrimalDual inner encodings. + /// + [TestMethod] + public void TestTEInnerEncodings() + { + var encodings = new[] { "KKT", "PrimalDual" }; + + foreach (var encoding in encodings) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--innerencoding", encoding, + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + var expected = encoding == "KKT" + ? InnerRewriteMethodChoice.KKT + : InnerRewriteMethodChoice.PrimalDual; + Assert.AreEqual(expected, opts.InnerEncoding, $"InnerEncoding mismatch for {encoding}"); + }); + } + } + + /// + /// Test PrimalDual with various demand lists. + /// + [TestMethod] + public void TestTEPrimalDualDemandLists() + { + var demandLists = new[] + { + "0", + "0,5", + "0,5,10", + "0,5,10,15,20", + "0,1,2,3,4,5,6,7,8,9,10", + }; + + foreach (var demandList in demandLists) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--innerencoding", "PrimalDual", + "--demandlist", demandList, + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(demandList, opts.DemandList, $"DemandList mismatch for {demandList}"); + + // Verify parsing + var parsed = opts.DemandList.Split(',').Select(double.Parse).ToList(); + var expectedCount = demandList.Split(',').Length; + Assert.AreEqual(expectedCount, parsed.Count); + }); + } + } + + #endregion + + #region Traffic Engineering - Clustering Options + + /// + /// Test clustering enable/disable. + /// + [TestMethod] + public void TestTEClusteringToggle() + { + // Clustering disabled (default) + var args1 = new string[] { "--problemType", "TrafficEngineering" }; + CommandLine.Parser.Default.ParseArguments(args1).WithParsed(opts => + { + Assert.AreEqual(false, opts.EnableClustering); + }); + + // Clustering enabled + var args2 = new string[] + { + "--problemType", "TrafficEngineering", + "--enableclustering", "true", + }; + CommandLine.Parser.Default.ParseArguments(args2).WithParsed(opts => + { + Assert.AreEqual(true, opts.EnableClustering); + }); + } + + /// + /// Test clustering with various cluster counts. + /// + [TestMethod] + public void TestTEClusteringNumClusters() + { + var clusterCounts = new[] { 2, 3, 4, 5, 10 }; + + foreach (var count in clusterCounts) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--enableclustering", "true", + "--numclusters", count.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(count, opts.NumClusters, $"NumClusters mismatch for {count}"); + }); + } + } + + /// + /// Test clustering versions. + /// + [TestMethod] + public void TestTEClusteringVersions() + { + var versions = new[] { 1, 2, 3 }; + + foreach (var version in versions) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--enableclustering", "true", + "--clusterversion", version.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(version, opts.ClusterVersion, $"ClusterVersion mismatch for {version}"); + }); + } + } + + #endregion + + #region Traffic Engineering - Path Options + + /// + /// Test various path counts. + /// + [TestMethod] + public void TestTEPathCounts() + { + var pathCounts = new[] { 1, 2, 3, 4, 5 }; + + foreach (var paths in pathCounts) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--paths", paths.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(paths, opts.Paths, $"Paths mismatch for {paths}"); + }); + } + } + + #endregion + + #region Traffic Engineering - Demand Constraints + + /// + /// Test demand upper bound values. + /// + [TestMethod] + public void TestTEDemandUpperBounds() + { + var bounds = new[] { -1.0, 10.0, 50.0, 100.0, 1000.0 }; + + foreach (var bound in bounds) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--demandub", bound.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(bound, opts.DemandUB, $"DemandUB mismatch for {bound}"); + }); + } + } + + /// + /// Test max density values. + /// + [TestMethod] + public void TestTEMaxDensity() + { + var densities = new[] { 0.1, 0.25, 0.5, 0.75, 1.0 }; + + foreach (var density in densities) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--maxdensity", density.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(density, opts.MaxDensity, $"MaxDensity mismatch for {density}"); + }); + } + } + + #endregion + + #region Bin Packing - FFD Method Combinations + + /// + /// Test all FFD methods with various bin/item counts. + /// + [TestMethod] + public void TestBPAllFFMethodsWithSizes() + { + var ffMethods = new[] { "FF", "FFDSum", "FFDProd", "FFDDiv" }; + var configs = new[] + { + (bins: 4, items: 6, optimal: 2), + (bins: 6, items: 9, optimal: 3), + (bins: 8, items: 12, optimal: 4), + (bins: 10, items: 15, optimal: 5), + }; + + foreach (var method in ffMethods) + { + foreach (var config in configs) + { + var args = new string[] + { + "--problemType", "BinPacking", + "--ffMethod", method, + "--numBins", config.bins.ToString(), + "--numDemands", config.items.ToString(), + "--optimalBins", config.optimal.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(config.bins, opts.NumBins); + Assert.AreEqual(config.items, opts.NumDemands); + Assert.AreEqual(config.optimal, opts.OptimalBins); + }); + + result.WithNotParsed(errors => + { + Assert.Fail($"Parse failed for {method} with config ({config.bins}, {config.items}, {config.optimal})"); + }); + } + } + } + + /// + /// Test BinPacking with various dimension counts. + /// + [TestMethod] + public void TestBPDimensions() + { + var dimensions = new[] { 1, 2, 3, 4, 5 }; + + foreach (var dim in dimensions) + { + // Build capacity string + var capacities = string.Join(",", Enumerable.Repeat("1.00001", dim)); + + var args = new string[] + { + "--problemType", "BinPacking", + "--numDimensions", dim.ToString(), + "--binCapacity", capacities, + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(dim, opts.NumDimensions, $"NumDimensions mismatch for {dim}"); + + var parsedCapacities = opts.BinCapacity.Split(',').Select(double.Parse).ToList(); + Assert.AreEqual(dim, parsedCapacities.Count, $"Capacity count mismatch for {dim}"); + }); + } + } + + /// + /// Test BinPacking with various bin capacities. + /// + [TestMethod] + public void TestBPBinCapacities() + { + var capacityConfigs = new[] + { + "1.0,1.0", + "1.00001,1.00001", + "1.5,1.5", + "2.0,1.0", + "1.0,2.0,1.5", + }; + + foreach (var capacities in capacityConfigs) + { + var args = new string[] + { + "--problemType", "BinPacking", + "--binCapacity", capacities, + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(capacities, opts.BinCapacity, $"BinCapacity mismatch for {capacities}"); + }); + } + } + + /// + /// Test BinPacking symmetry breaking toggle. + /// + [TestMethod] + public void TestBPSymmetryBreaking() + { + // Default (false) + var args1 = new string[] { "--problemType", "BinPacking" }; + CommandLine.Parser.Default.ParseArguments(args1).WithParsed(opts => + { + Assert.AreEqual(false, opts.BreakSymmetry); + }); + + // Enabled + var args2 = new string[] { "--problemType", "BinPacking", "--breakSymmetry", "true" }; + CommandLine.Parser.Default.ParseArguments(args2).WithParsed(opts => + { + Assert.AreEqual(true, opts.BreakSymmetry); + }); + } + + #endregion + + #region PIFO - Parameter Combinations + + /// + /// Test PIFO with various packet counts. + /// + [TestMethod] + public void TestPIFOPacketCounts() + { + var packetCounts = new[] { 10, 12, 15, 18, 20, 24 }; + + foreach (var packets in packetCounts) + { + var args = new string[] + { + "--problemType", "PIFO", + "--numPackets", packets.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(packets, opts.NumPackets, $"NumPackets mismatch for {packets}"); + }); + } + } + + /// + /// Test PIFO with various rank values. + /// + [TestMethod] + public void TestPIFORankValues() + { + var ranks = new[] { 4, 6, 8, 10, 12, 16 }; + + foreach (var rank in ranks) + { + var args = new string[] + { + "--problemType", "PIFO", + "--maxRank", rank.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(rank, opts.MaxRank, $"MaxRank mismatch for {rank}"); + }); + } + } + + /// + /// Test PIFO with various queue configurations. + /// + [TestMethod] + public void TestPIFOQueueConfigs() + { + var configs = new[] + { + (queues: 2, queueSize: 8), + (queues: 4, queueSize: 12), + (queues: 6, queueSize: 15), + (queues: 8, queueSize: 20), + }; + + foreach (var config in configs) + { + var args = new string[] + { + "--problemType", "PIFO", + "--numQueues", config.queues.ToString(), + "--maxQueueSize", config.queueSize.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(config.queues, opts.NumQueues); + Assert.AreEqual(config.queueSize, opts.MaxQueueSize); + }); + } + } + + /// + /// Test PIFO AIFO parameters. + /// + [TestMethod] + public void TestPIFOAIFOParams() + { + var configs = new[] + { + (windowSize: 8, burstParam: 0.05), + (windowSize: 12, burstParam: 0.1), + (windowSize: 15, burstParam: 0.15), + (windowSize: 20, burstParam: 0.2), + }; + + foreach (var config in configs) + { + var args = new string[] + { + "--problemType", "PIFO", + "--windowSize", config.windowSize.ToString(), + "--burstParam", config.burstParam.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(config.windowSize, opts.WindowSize); + Assert.AreEqual(config.burstParam, opts.BurstParam); + }); + } + } + + #endregion + + #region Failure Analysis - Parameter Combinations + + /// + /// Test FailureAnalysis with various failure counts. + /// + [TestMethod] + public void TestFAFailureCounts() + { + var failureCounts = new[] { 1, 2, 3, 4, 5 }; + + foreach (var failures in failureCounts) + { + var args = new string[] + { + "--problemType", "FailureAnalysis", + "--maxNumFailures", failures.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(failures, opts.MaxNumFailures, $"MaxNumFailures mismatch for {failures}"); + }); + } + } + + /// + /// Test FailureAnalysis with various extra path counts. + /// + [TestMethod] + public void TestFAExtraPaths() + { + var extraPaths = new[] { 1, 2, 3, 4, 5 }; + + foreach (var paths in extraPaths) + { + var args = new string[] + { + "--problemType", "FailureAnalysis", + "--numExtraPaths", paths.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(paths, opts.NumExtraPaths, $"NumExtraPaths mismatch for {paths}"); + }); + } + } + + /// + /// Test FailureAnalysis failure probability thresholds. + /// + [TestMethod] + public void TestFAFailureProbThresholds() + { + var thresholds = new[] { 0.0, 0.1, 0.25, 0.5, 0.75, 1.0 }; + + foreach (var threshold in thresholds) + { + var args = new string[] + { + "--problemType", "FailureAnalysis", + "--failureProbThreshold", threshold.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(threshold, opts.FailureProbThreshold, $"FailureProbThreshold mismatch for {threshold}"); + }); + } + } + + /// + /// Test FailureAnalysis scenario probability thresholds. + /// + [TestMethod] + public void TestFAScenarioProbThresholds() + { + var thresholds = new[] { 0.0, 0.01, 0.05, 0.1 }; + + foreach (var threshold in thresholds) + { + var args = new string[] + { + "--problemType", "FailureAnalysis", + "--scenarioProbThreshold", threshold.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(threshold, opts.ScenarioProbThreshold, $"ScenarioProbThreshold mismatch for {threshold}"); + }); + } + } + + /// + /// Test FailureAnalysis topology toggle. + /// + [TestMethod] + public void TestFATopologyToggle() + { + // With flag = true + var args1 = new string[] + { + "--problemType", "FailureAnalysis", + "--useDefaultTopology", + }; + CommandLine.Parser.Default.ParseArguments(args1).WithParsed(opts => + { + Assert.AreEqual(true, opts.UseDefaultTopology); + }); + + // Without flag = false (uses default) + var args2 = new string[] + { + "--problemType", "FailureAnalysis", + "--topologyFile", "custom.json", "--useDefaultTopology", "false", + }; + CommandLine.Parser.Default.ParseArguments(args2).WithParsed(opts => + { + Assert.AreEqual(false, opts.UseDefaultTopology); + Assert.AreEqual("custom.json", opts.TopologyFile); + }); + } + + #endregion + + #region Common Options - Timeout and Threads + + /// + /// Test various timeout values. + /// + [TestMethod] + public void TestTimeoutValues() + { + var timeouts = new[] { 10.0, 60.0, 300.0, 1000.0, 3600.0 }; + + foreach (var timeout in timeouts) + { + var args = new string[] + { + "--problemType", "BinPacking", + "--timeout", timeout.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(timeout, opts.Timeout, $"Timeout mismatch for {timeout}"); + }); + } + } + + /// + /// Test various Gurobi thread counts. + /// + [TestMethod] + public void TestGurobiThreads() + { + var threadCounts = new[] { 0, 1, 2, 4, 8 }; + + foreach (var threads in threadCounts) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--gurobithreads", threads.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(threads, opts.NumGurobiThreads, $"NumGurobiThreads mismatch for {threads}"); + }); + } + } + + /// + /// Test seed values for reproducibility. + /// + [TestMethod] + public void TestSeedValues() + { + var seeds = new[] { 0, 1, 42, 12345, 999999 }; + + foreach (var seed in seeds) + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--seed", seed.ToString(), + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(seed, opts.Seed, $"Seed mismatch for {seed}"); + }); + } + } + + #endregion + + #region Common Options - Verbose and Debug + + /// + /// Test verbose flag. + /// + [TestMethod] + public void TestVerboseFlag() + { + // Without verbose + var args1 = new string[] { "--problemType", "BinPacking" }; + CommandLine.Parser.Default.ParseArguments(args1).WithParsed(opts => + { + Assert.AreEqual(false, opts.Verbose); + }); + + // With verbose + var args2 = new string[] { "--problemType", "BinPacking", "--verbose" }; + CommandLine.Parser.Default.ParseArguments(args2).WithParsed(opts => + { + Assert.AreEqual(true, opts.Verbose); + }); + + // With -v shorthand + var args3 = new string[] { "--problemType", "BinPacking", "-v" }; + CommandLine.Parser.Default.ParseArguments(args3).WithParsed(opts => + { + Assert.AreEqual(true, opts.Verbose); + }); + } + + /// + /// Test debug flag. + /// + [TestMethod] + public void TestDebugFlag() + { + // Without debug + var args1 = new string[] { "--problemType", "BinPacking" }; + CommandLine.Parser.Default.ParseArguments(args1).WithParsed(opts => + { + Assert.AreEqual(false, opts.Debug); + }); + + // With debug + var args2 = new string[] { "--problemType", "BinPacking", "--debug" }; + CommandLine.Parser.Default.ParseArguments(args2).WithParsed(opts => + { + Assert.AreEqual(true, opts.Debug); + }); + } + + #endregion + + #region Complex Combination Tests + + /// + /// Test TE with Pop heuristic and all common options. + /// + [TestMethod] + public void TestTEPopFullConfiguration() + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--topologyFile", "Swan.json", + "--heuristic", "Pop", + "--solver", "Gurobi", + "--paths", "2", + "--slices", "3", + "--method", "Direct", + "--innerencoding", "KKT", + "--timeout", "300", + "--gurobithreads", "1", + "--seed", "42", + "--verbose", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.TrafficEngineering, opts.ProblemType); + Assert.AreEqual("Swan.json", opts.TopologyFile); + Assert.AreEqual(Heuristic.Pop, opts.Heuristic); + Assert.AreEqual(SolverChoice.Gurobi, opts.SolverChoice); + Assert.AreEqual(2, opts.Paths); + Assert.AreEqual(3, opts.PopSlices); + Assert.AreEqual(MethodChoice.Direct, opts.Method); + Assert.AreEqual(InnerRewriteMethodChoice.KKT, opts.InnerEncoding); + Assert.AreEqual(300, opts.Timeout); + Assert.AreEqual(1, opts.NumGurobiThreads); + Assert.AreEqual(42, opts.Seed); + Assert.AreEqual(true, opts.Verbose); + }); + + result.WithNotParsed(errors => + { + Assert.Fail("Failed to parse TE Pop full configuration"); + }); + } + + /// + /// Test TE with DemandPinning and PrimalDual. + /// + [TestMethod] + public void TestTEDemandPinningPrimalDualConfig() + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--heuristic", "DemandPinning", + "--pinthreshold", "0.5", + "--innerencoding", "PrimalDual", + "--demandlist", "0,5,10,15,20", + "--paths", "3", + "--method", "Direct", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(Heuristic.DemandPinning, opts.Heuristic); + Assert.AreEqual(0.5, opts.DemandPinningThreshold); + Assert.AreEqual(InnerRewriteMethodChoice.PrimalDual, opts.InnerEncoding); + Assert.AreEqual("0,5,10,15,20", opts.DemandList); + Assert.AreEqual(3, opts.Paths); + }); + } + + /// + /// Test TE with clustering enabled. + /// + [TestMethod] + public void TestTEClusteringFullConfig() + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--enableclustering", "true", + "--numclusters", "4", + "--clusterversion", "3", + "--interclustersamples", "10", + "--nodespercluster", "5", + "--numinterclusterquantization", "5", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(true, opts.EnableClustering); + Assert.AreEqual(4, opts.NumClusters); + Assert.AreEqual(3, opts.ClusterVersion); + Assert.AreEqual(10, opts.NumInterClusterSamples); + Assert.AreEqual(5, opts.NumNodesPerCluster); + Assert.AreEqual(5, opts.NumInterClusterQuantizations); + }); + } + + /// + /// Test BinPacking with all options. + /// + [TestMethod] + public void TestBPFullConfiguration() + { + var args = new string[] + { + "--problemType", "BinPacking", + "--solver", "Gurobi", + "--numBins", "8", + "--numDemands", "12", + "--numDimensions", "3", + "--binCapacity", "1.5,1.5,1.5", + "--optimalBins", "4", + "--ffMethod", "FFDProd", + "--breakSymmetry", "true", + "--timeout", "120", + "--verbose", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.BinPacking, opts.ProblemType); + Assert.AreEqual(SolverChoice.Gurobi, opts.SolverChoice); + Assert.AreEqual(8, opts.NumBins); + Assert.AreEqual(12, opts.NumDemands); + Assert.AreEqual(3, opts.NumDimensions); + Assert.AreEqual("1.5,1.5,1.5", opts.BinCapacity); + Assert.AreEqual(4, opts.OptimalBins); + Assert.AreEqual(FFDMethodChoice.FFDProd, opts.FFMethod); + Assert.AreEqual(true, opts.BreakSymmetry); + Assert.AreEqual(120, opts.Timeout); + Assert.AreEqual(true, opts.Verbose); + }); + } + + /// + /// Test PIFO with all options. + /// + [TestMethod] + public void TestPIFOFullConfiguration() + { + var args = new string[] + { + "--problemType", "PIFO", + "--solver", "Gurobi", + "--numPackets", "24", + "--maxRank", "10", + "--numQueues", "6", + "--maxQueueSize", "16", + "--windowSize", "14", + "--burstParam", "0.15", + "--timeout", "180", + "--verbose", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.PIFO, opts.ProblemType); + Assert.AreEqual(24, opts.NumPackets); + Assert.AreEqual(10, opts.MaxRank); + Assert.AreEqual(6, opts.NumQueues); + Assert.AreEqual(16, opts.MaxQueueSize); + Assert.AreEqual(14, opts.WindowSize); + Assert.AreEqual(0.15, opts.BurstParam); + Assert.AreEqual(180, opts.Timeout); + }); + } + + /// + /// Test FailureAnalysis with all options. + /// + [TestMethod] + public void TestFAFullConfiguration() + { + var args = new string[] + { + "--problemType", "FailureAnalysis", + "--solver", "Gurobi", + "--useDefaultTopology", "true", + "--maxNumFailures", "2", + "--numExtraPaths", "2", + "--demandlist", "0,5,10,15", + "--failureProbThreshold", "0.15", + "--scenarioProbThreshold", "0.01", + "--innerencoding", "PrimalDual", + "--timeout", "240", + "--verbose", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + Assert.AreEqual(ProblemType.FailureAnalysis, opts.ProblemType); + Assert.AreEqual(true, opts.UseDefaultTopology); + Assert.AreEqual(2, opts.MaxNumFailures); + Assert.AreEqual(2, opts.NumExtraPaths); + Assert.AreEqual("0,5,10,15", opts.DemandList); + Assert.AreEqual(0.15, opts.FailureProbThreshold); + Assert.AreEqual(0.01, opts.ScenarioProbThreshold); + Assert.AreEqual(InnerRewriteMethodChoice.PrimalDual, opts.InnerEncoding); + Assert.AreEqual(240, opts.Timeout); + }); + } + + #endregion + + #region Smoke Tests - Actual Runner Initialization + + /// + /// Smoke test: BinPacking runner can be initialized. + /// + [TestMethod] + public void SmokeTestBPRunnerInit() + { + var args = new string[] + { + "--problemType", "BinPacking", + "--solver", "Gurobi", + "--numBins", "4", + "--numDemands", "6", + "--numDimensions", "2", + "--optimalBins", "2", + "--timeout", "30", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + CliOptions.Instance = opts; + + // Parse capacities + var binCapacities = opts.BinCapacity.Split(',').Select(double.Parse).ToList(); + while (binCapacities.Count < opts.NumDimensions) + { + binCapacities.Add(1.00001); + } + + // Create components + var solver = new GurobiSOS(timeout: opts.Timeout, verbose: 0); + var bins = new Bins(opts.NumBins, binCapacities); + var optimalEncoder = new VBPOptimalEncoder( + solver, opts.NumDemands, opts.NumDimensions, BreakSymmetry: opts.BreakSymmetry); + var ffdEncoder = new FFDItemCentricEncoder( + solver, opts.NumDemands, opts.NumDimensions); + var adversarialGenerator = new VBPAdversarialInputGenerator( + bins, opts.NumDemands, opts.NumDimensions); + + Assert.IsNotNull(solver); + Assert.IsNotNull(bins); + Assert.IsNotNull(optimalEncoder); + Assert.IsNotNull(ffdEncoder); + Assert.IsNotNull(adversarialGenerator); + }); + } + + /// + /// Smoke test: PIFO runner can be initialized. + /// + [TestMethod] + public void SmokeTestPIFORunnerInit() + { + var args = new string[] + { + "--problemType", "PIFO", + "--solver", "Gurobi", + "--numPackets", "18", + "--maxRank", "8", + "--numQueues", "4", + "--maxQueueSize", "12", + "--timeout", "1000", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + CliOptions.Instance = opts; + + var solver = new GurobiSOS(timeout: opts.Timeout, verbose: 0); + + var spPifoEncoder = new SPPIFOWithDropAvgDelayEncoder( + solver, opts.NumPackets, opts.NumQueues, opts.MaxRank, opts.MaxQueueSize); + + var aifoEncoder = new AIFOAvgDelayEncoder( + solver, opts.NumPackets, opts.MaxRank, opts.MaxQueueSize, opts.WindowSize, opts.BurstParam); + + var adversarialGenerator = new PIFOAdversarialInputGenerator( + opts.NumPackets, opts.MaxRank); + + Assert.IsNotNull(solver); + Assert.IsNotNull(spPifoEncoder); + Assert.IsNotNull(aifoEncoder); + Assert.IsNotNull(adversarialGenerator); + }); + } + + /// + /// Smoke test: TE components can be initialized. + /// + [TestMethod] + public void SmokeTestTERunnerInit() + { + var args = new string[] + { + "--problemType", "TrafficEngineering", + "--heuristic", "Pop", + "--solver", "Gurobi", + "--paths", "2", + "--slices", "2", + "--timeout", "30", + }; + + var result = CommandLine.Parser.Default.ParseArguments(args); + + result.WithParsed(opts => + { + CliOptions.Instance = opts; + + var solver = new GurobiSOS(timeout: opts.Timeout, verbose: 0); + var optimalEncoder = new TEMaxFlowOptimalEncoder(solver, opts.Paths); + + // Create simple test topology + var topology = new Topology(); + topology.AddNode("a"); + topology.AddNode("b"); + topology.AddNode("c"); + topology.AddEdge("a", "b", capacity: 10); + topology.AddEdge("b", "c", capacity: 10); + topology.AddEdge("a", "c", capacity: 5); + + var partition = topology.RandomPartition(opts.PopSlices); + var popEncoder = new PopEncoder( + solver, maxNumPaths: opts.Paths, numPartitions: opts.PopSlices, demandPartitions: partition); + + var adversarialGenerator = new TEAdversarialInputGenerator( + topology, opts.Paths); + + Assert.IsNotNull(solver); + Assert.IsNotNull(optimalEncoder); + Assert.IsNotNull(popEncoder); + Assert.IsNotNull(adversarialGenerator); + }); + } + + #endregion + } +} \ No newline at end of file diff --git a/MetaOptimize.Test/FailureAnalysisBasicTests.cs b/MetaOptimize.Test/FailureAnalysisBasicTests.cs index ad820cda9..1928e01fd 100644 --- a/MetaOptimize.Test/FailureAnalysisBasicTests.cs +++ b/MetaOptimize.Test/FailureAnalysisBasicTests.cs @@ -2,14 +2,11 @@ namespace MetaOptimize.Test { using System; using System.Collections.Generic; - using System.Configuration.Assemblies; - using System.Diagnostics.CodeAnalysis; using System.Linq; - using System.Runtime.InteropServices; using MetaOptimize; using MetaOptimize.FailureAnalysis; using Microsoft.VisualStudio.TestTools.UnitTesting; - using Microsoft.Z3; + /// /// The tests for the basic failure analysis class. /// @@ -410,6 +407,7 @@ public void adversarialGeneratorWithProbabilityThreshold() (optimalSol, failureSol) = adversarialGenerator.MaximizeOptimalityGap(optimalEncoder, optimalCutEncoder, innerEncoding: InnerRewriteMethodChoice.PrimalDual, constrainedDemands: demands, maxNumFailures: 2, demandList: demandList, numExtraPaths: 1, lagFailureProbabilities: probs, failureProbThreshold: 0.1); Assert.IsTrue(Utils.IsApproximately(2, optimalSol.MaxObjective - failureSol.MaxObjective)); } + /// /// Tests that the instance based gap generator is correct. /// diff --git a/MetaOptimize.Test/MetaOptimize.Test.csproj b/MetaOptimize.Test/MetaOptimize.Test.csproj index 79fe2b92d..b88ba2eec 100644 --- a/MetaOptimize.Test/MetaOptimize.Test.csproj +++ b/MetaOptimize.Test/MetaOptimize.Test.csproj @@ -1,29 +1,28 @@  - - net6.0 - disable - + net8.0 false - - x64 - x64 + true - - - - - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + - + \ No newline at end of file diff --git a/MetaOptimize/MetaOptimize.csproj b/MetaOptimize/MetaOptimize.csproj index 13f478f8f..d1e1d65eb 100644 --- a/MetaOptimize/MetaOptimize.csproj +++ b/MetaOptimize/MetaOptimize.csproj @@ -1,26 +1,14 @@  - - - Library - net6.0 - © Microsoft Corporation. All rights reserved. - Microsoft - x64 - x64 - - - - - - - - - - + + + + + + + - <_Parameter1>$(MSBuildProjectName)Tests diff --git a/MetaOptimize/SolverZen.cs b/MetaOptimize/SolverZen.cs index 9f476eddc..2ccc6424f 100644 --- a/MetaOptimize/SolverZen.cs +++ b/MetaOptimize/SolverZen.cs @@ -10,6 +10,8 @@ namespace MetaOptimize using System.Linq; using Gurobi; using ZenLib; + using ZenLib.ModelChecking; + /// /// An interface for an optimization solver. /// @@ -36,7 +38,7 @@ public class SolverZen : ISolver, ZenSolution> /// static SolverZen() { - ZenLib.Settings.UseLargeStack = true; + ZenSettings.UseLargeStack = true; } /// diff --git a/README.md b/README.md index 5a1f408dc..7a3b994c6 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,76 @@ dotnet run We also provide many unittests that can serve as a starting point in `MetaOptimize.Test`. +### Command Line Interface + +MetaOpt provides a unified CLI for all problem types. To see all available options: +```bash +cd MetaOptimize.Cli +dotnet run -- --help +``` + +Or from the parent directory: +```bash +dotnet run --project MetaOptimize.Cli -- --help +``` + +#### Problem Types + +Use `--problemType` to select the optimization problem: + +| Problem Type | Description | +|--------------|-------------| +| `TrafficEngineering` | Network routing optimization | +| `BinPacking` | Vector bin packing | +| `PIFO` | Packet scheduling | +| `FailureAnalysis` | Network failure resilience (Raha) | + +#### Examples + +**Traffic Engineering:** +```bash +dotnet run -- --problemType TrafficEngineering --topologyFile ../Topologies/simple.json --heuristic Pop --innerencoding PrimalDual --demandlist "0,0.25,0.5,0.75,1.0" +``` + +**Bin Packing:** +```bash +dotnet run -- --problemType BinPacking --numBins 6 --numDemands 9 --numDimensions 2 --ffMethod FFDSum --optimalBins 3 +``` + +**PIFO Scheduling:** +```bash +dotnet run -- --problemType PIFO --pifoMethod1 SPPIFO --pifoMethod2 AIFO --considerPktDrop true --numPackets 18 --maxRank 8 +``` + +**Failure Analysis:** +```bash +dotnet run -- --problemType FailureAnalysis --useDefaultTopology true --maxNumFailures 1 --innerencoding KKT +``` + +#### Solver Selection + +Use `--solver` to select the optimization solver: + +| Solver | Description | +|--------|-------------| +| `Gurobi` | Commercial MIP solver (default, requires license) | +| `Zen` | SMT solver based on Z3 (open source) | +| `OrTools` | Google's CP-SAT solver (open source, limited features) | + +Example: +```bash +dotnet run -- --problemType BinPacking --solver OrTools --numBins 6 --numDemands 9 +``` + +#### Common Options + +| Option | Default | Description | +|--------|---------|-------------| +| `--timeout` | ∞ | Solver timeout in seconds | +| `--verbose` | false | Enable detailed output | +| `--innerencoding` | KKT | Bilevel encoding method (KKT or PrimalDual) | +| `--seed` | 1 | Random seed for reproducibility | + ## Analyzing heuristics using MetaOpt

diff --git a/Topologies/simple.json b/Topologies/simple.json new file mode 100644 index 000000000..ad4231c91 --- /dev/null +++ b/Topologies/simple.json @@ -0,0 +1,17 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { "id": "a" }, + { "id": "b" }, + { "id": "c" }, + { "id": "d" } + ], + "links": [ + { "source": "a", "target": "b", "capacity": 10 }, + { "source": "a", "target": "c", "capacity": 10 }, + { "source": "b", "target": "d", "capacity": 10 }, + { "source": "c", "target": "d", "capacity": 10 } + ] +} \ No newline at end of file