### Quick Example Source: https://github.com/robin-vjc/nsopy/blob/master/README.md A basic example demonstrating how to use nsopy to minimize a function. ```APIDOC ## Usage ### Quick Example We seek to minimize a function obtained by taking the `max` over a set of affine functions. The feasible set considered is the set of non-negative real numbers, i.e., for which the projection operation is straightforward.

Example

It is straightforward to see that the optimum is at `x* = 2.25`; we can solve this optimization problem numerically as follows: ```python import numpy as np def oracle(x_k): # evaluation of the f_i components at x_k fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) # function value at x_k diff_fi = [-2, -1.0/3.0, 1] # gradients of the components max_i = fi_x_k.index(f_x_k) # subgradient at x_k is the gradient of the active function component; cast as (1x1 dimensional) np.array diff_f_xk = np.array([diff_fi[max_i], ]) return 0, f_x_k, diff_f_xk def projection_function(x_k): if x_k is 0: return np.array([0,]) else: return np.maximum(x_k, 0) ``` Instantiation of method and logger, solve and print ```python from nsopy.methods.subgradient import SubgradientMethod from nsopy.loggers import GenericMethodLogger method = SubgradientMethod(oracle, projection_function, stepsize_0=0.1, stepsize_rule='constant', sense='min') logger = GenericMethodLogger(method) for iteration in range(200): method.step() ``` Result: ``` >>> print(logger.x_k_iterates[-5:]) [2.1999999999999904, 2.216666666666657, 2.2333333333333236, 2.2499999999999902, 2.266666666666657] ``` ``` -------------------------------- ### Installation Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Install the nsopy library using pip. ```APIDOC ## Installation ```bash pip install nsopy ``` ``` -------------------------------- ### Instantiate Analytical Example Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/02. Application to Duality.ipynb Creates an instance of the AnalyticalExample class to be used with the dual methods. ```python inner_problem = AnalyticalExample() ``` -------------------------------- ### Install Nsopy using Pip Source: https://github.com/robin-vjc/nsopy/blob/master/docs/usage.md Install the nsopy library using pip. Ensure you are in your virtual environment. ```console (.venv) $ pip install lumache ``` -------------------------------- ### CuttingPlanesMethod Setup Source: https://context7.com/robin-vjc/nsopy/llms.txt Initializes the classical Cutting Planes Method, which approximates the objective function with a polyhedral structure. This method requires the Gurobi solver and defines an optimality gap tolerance. ```python import numpy as np from nsopy.methods.bundle import CuttingPlanesMethod from nsopy.loggers import GenericDualMethodLogger def oracle(x_k): fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) diff_fi = [-2, -1.0/3.0, 1] max_i = fi_x_k.index(f_x_k) diff_f_xk = np.array([diff_fi[max_i]]) return 0, f_x_k, diff_f_xk def projection_function(x_k): return np.maximum(x_k, 0) method = CuttingPlanesMethod( oracle=oracle, projection_function=projection_function, dimension=1, epsilon=0.01, # Optimality gap tolerance search_box_min=0, # Lower bound on search region search_box_max=10, # Upper bound on search region sense='min' ) # Optional: configure dual domain constraints # method.set_dual_domain(type='positive orthant') logger = GenericDualMethodLogger(method) ``` -------------------------------- ### Import Libraries and Set Up Environment Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/01. Analytical Example.ipynb Imports necessary libraries like numpy and matplotlib, and sets the working directory. This is a common setup for numerical and plotting tasks. ```python import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D %matplotlib notebook %cd .. ``` -------------------------------- ### UniversalFGM Implementation Source: https://context7.com/robin-vjc/nsopy/llms.txt Demonstrates the setup for the Universal Fast Gradient Method, an accelerated variant designed for optimal convergence rates. It uses an epsilon parameter for line search accuracy and can track the adaptive Lipschitz estimate. ```python import numpy as np from nsopy.methods.universal import UniversalFGM from nsopy.loggers import DualDgmFgmMethodLogger def oracle(x_k): fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) diff_fi = [-2, -1.0/3.0, 1] max_i = fi_x_k.index(f_x_k) diff_f_xk = np.array([diff_fi[max_i]]) return 0, f_x_k, diff_f_xk def projection_function(x_k): return np.maximum(x_k, 0) method = UniversalFGM( oracle=oracle, projection_function=projection_function, dimension=1, epsilon=0.5, averaging=False, sense='min' ) logger = DualDgmFgmMethodLogger(method) for _ in range(30): method.step() print(f"Solution: {logger.lambda_k_iterates[-1]}") print(f"Adaptive L_k: {logger.L_k_iterates[-1]}") # Track Lipschitz estimate ``` -------------------------------- ### SGMTripleAveraging for Quasi-Monotone Framework Source: https://context7.com/robin-vjc/nsopy/llms.txt Implements Subgradient Method with Triple Averaging, offering variants within Nesterov's quasi-monotone framework. This example shows the setup for a problem with a piecewise linear objective function. ```python import numpy as np from nsopy.methods.quasi_monotone import SGMTripleAveraging from nsopy.loggers import GenericDualMethodLogger def oracle(x_k): fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) diff_fi = [-2, -1.0/3.0, 1] max_i = fi_x_k.index(f_x_k) diff_f_xk = np.array([diff_fi[max_i]]) return 0, f_x_k, diff_f_xk def projection_function(x_k): return np.maximum(x_k, 0) ``` -------------------------------- ### MILP Lagrangian Dual Decomposition with nsopy Source: https://context7.com/robin-vjc/nsopy/llms.txt This example defines a MILP dual problem and compares different nsopy methods (Subgradient, Double Simple Averaging, Universal PGM) for solving it. It requires defining an oracle, projection function, and then iterating through the chosen methods. ```python import numpy as np from nsopy.methods.subgradient import SubgradientMethod from nsopy.methods.quasi_monotone import SGMDoubleSimpleAveraging from nsopy.methods.universal import UniversalPGM from nsopy.loggers import EnhancedDualMethodLogger class MILPDualProblem: """ MILP: min -0.5x1 - x2 + x3 s.t. 0.5x1 + 0.5x2 + x3 >= 1 (lambda_1) x1 + x2 <= 1 (lambda_2) 0 <= x1,x2,x3 <= 1, x1 binary Optimal dual: lambda* = [1, ~1.25], d* = -0.5 """ def __init__(self): self.dimension = 2 self.instance_name = 'MILP Example' self.instance_type = 'analytical' self.instance_subtype = 'lagrangian' def oracle(self, lambda_k): c = np.array([-0.5 - 0.5*lambda_k[0] + lambda_k[1], -1 - 0.5*lambda_k[0] + lambda_k[1], 1 - lambda_k[0]]) x_k = (c < 0).astype(float) diff_d_k = np.array([ 1 - 0.5*x_k[0] - 0.5*x_k[1] - x_k[2], # constraint 1 slack x_k[0] + x_k[1] - 1 # constraint 2 slack ]) d_k = np.dot(c, x_k) + lambda_k[0] - lambda_k[1] return x_k, d_k, diff_d_k def projection_function(self, lambda_k): return np.maximum(lambda_k, 0) problem = MILPDualProblem() # Compare different methods methods = { 'Subgradient 1/k': SubgradientMethod( problem.oracle, problem.projection_function, dimension=2, stepsize_rule='1/k', sense='max' ), 'Double Simple Averaging': SGMDoubleSimpleAveraging( problem.oracle, problem.projection_function, dimension=2, gamma=1.0, sense='max' ), 'Universal PGM': UniversalPGM( problem.oracle, problem.projection_function, dimension=2, epsilon=0.1, sense='max' ) } results = {} for name, method in methods.items(): logger = EnhancedDualMethodLogger(method) for _ in range(100): method.step() results[name] = { 'lambda': logger.lambda_k_iterates[-1], 'd_k': logger.d_k_iterates[-1], 'oracle_calls': logger.oracle_calls[-1] } for name, res in results.items(): print(f"{name}:") print(f" lambda* = {res['lambda']}") print(f" d* = {res['d_k']:.4f}") print(f" Oracle calls: {res['oracle_calls']}") ``` -------------------------------- ### SGMTripleAveraging Method Variants Source: https://context7.com/robin-vjc/nsopy/llms.txt Demonstrates the setup for two variants of the SGMTripleAveraging method, differing in their internal parameter updates (a_t and gamma_t). ```python method_v1 = SGMTripleAveraging( oracle=oracle, projection_function=projection_function, dimension=1, variant=1, # Options: 1 or 2 gamma=1.0, sense='min' ) method_v2 = SGMTripleAveraging( oracle=oracle, projection_function=projection_function, dimension=1, variant=2, gamma=1.0, sense='min' ) logger_v1 = GenericDualMethodLogger(method_v1) logger_v2 = GenericDualMethodLogger(method_v2) for _ in range(100): method_v1.step() method_v2.step() print(f"Variant 1 result: {logger_v1.lambda_k_iterates[-1]}") print(f"Variant 2 result: {logger_v2.lambda_k_iterates[-1]}") ``` -------------------------------- ### Define Analytical Example Class Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/02. Application to Duality.ipynb Defines an AnalyticalExample class to represent an optimization problem with oracle, projection, and other necessary methods. This class is used to instantiate problems for the dual methods. ```python class AnalyticalExample(object): def __init__(self): self.dimension = 2 def oracle(self, lambda_k): assert type(lambda_k) == np.ndarray, 'WARNING: lambda_k should be a numpy array.' c = np.zeros(3, dtype=float) x_k = np.zeros(3, dtype=float) c[0] = - 0.5 - 0.5*lambda_k[0] + lambda_k[1] c[1] = - 1 - 0.5*lambda_k[0] + lambda_k[1] c[2] = + 1 - lambda_k[0] x_k[0] = 1 if c[0] < 0 else 0 x_k[1] = 1 if c[1] < 0 else 0 x_k[2] = 1 if c[2] < 0 else 0 diff_d_k = np.zeros(2) diff_d_k[0] = 1 - 0.5*x_k[0] - 0.5*x_k[1] - x_k[2] diff_d_k[1] = x_k[0] + x_k[1] -1 d_k = c[0]*x_k[0] + c[1]*x_k[1] + c[2] *x_k[2] + lambda_k[0] - lambda_k[1] return x_k, d_k, diff_d_k def projection_function(self, lambda_k): # initial point lambda_0 if isinstance(lambda_k, int) and lambda_k == 0: return np.zeros(self.dimension) # project lambda_k on the positive orthant return np.maximum(lambda_k, 0) ``` -------------------------------- ### Get Random Ingredients with Nsopy Source: https://github.com/robin-vjc/nsopy/blob/master/docs/usage.md Use the `get_random_ingredients` function from the lumache library to retrieve a list of random ingredients. The `kind` parameter is optional and can be 'meat', 'fish', or 'veggies'. ```python >>> import lumache >>> lumache.get_random_ingredients() ['shells', 'gorgonzola', 'parsley'] ``` -------------------------------- ### Initialize OptimalShedding and UniversalPGM Optimizer Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Sets up the OptimalShedding problem and initializes the UniversalPGM optimizer. The optimizer requires the oracle and projection functions from the problem, along with a specified epsilon value. ```python problem = OptimalShedding(n_customers=20, n_products=2) optimizer = UniversalPGM(problem.oracle, problem.projection_function, epsilon=0.01) # optimizer = UniversalPGM(problem.oracle, problem.projection_function, epsilon=0.1) # optimizer = SubgradientMethod(problem.oracle, problem.projection_function, stepsize_0=0.1, sense='max') logger = GenericDualMethodLogger(optimizer) ``` -------------------------------- ### Initialize and Run Dual Method Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/02. Application to Duality.ipynb Imports various dual methods from nsopy, instantiates UniversalPGM with the analytical problem, and runs it for a specified number of iterations. The GenericDualMethodLogger is used to track the method's progress. ```python from nsopy.methods.quasi_monotone import SGMDoubleSimpleAveraging as DSA from nsopy.methods.quasi_monotone import SGMTripleAveraging as TA from nsopy.methods.subgradient import SubgradientMethod as SG from nsopy.methods.universal import UniversalPGM as UPGM from nsopy.methods.universal import UniversalDGM as UDGM from nsopy.methods.universal import UniversalFGM as UFGM from nsopy.loggers import GenericDualMethodLogger dual_method = UPGM(inner_problem.oracle, inner_problem.projection_function, epsilon=0.01) method_logger = GenericDualMethodLogger(dual_method) for iteration in range(60): dual_method.dual_step() ``` -------------------------------- ### Initialize UniversalPGM optimizer and logger Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Sets up the UniversalPGM optimizer with the defined problem's oracle and projection functions, and initializes a logger to track optimization iterates. ```python problem = AnalyticalExample() optimizer = UniversalPGM(problem.oracle, problem.projection_function, epsilon=0.01) logger = GenericDualMethodLogger(optimizer) ``` -------------------------------- ### Instantiate and Run Subgradient Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Instantiates the SubgradientMethod with the defined oracle and projection function, sets up a logger, and runs the optimization for 200 iterations. ```python from nsopy.methods.subgradient import SubgradientMethod from nsopy.loggers import GenericMethodLogger method = SubgradientMethod(oracle, projection_function, stepsize_0=0.1, stepsize_rule='constant', sense='min') logger = GenericMethodLogger(method) for iteration in range(200): method.step() ``` -------------------------------- ### Initialize and Run Optimization Method Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/01. Analytical Example.ipynb Initializes a UniversalPGM optimization method and runs it for a specified number of iterations. It also sets a custom initial point. ```python # method = DSA(oracle, projection_function, dimension=2, gamma=0.5) # method = TA(oracle, projection_function, dimension=2, variant=2, gamma=0.5) # method = SG(oracle, projection_function, dimension=2) method = UPGM(oracle, projection_function, dimension=2, epsilon=10, averaging=True) # method = UDGM(oracle, projection_function, dimension=2, epsilon=1.0) # method = UFGM(oracle, projection_function, dimension=2, epsilon=1.0) method_logger = GenericDualMethodLogger(method) # start from an different initial point x_0 = np.array([2.01,2.01]) method.lambda_hat_k = x_0 for iteration in range(100): method.dual_step() ``` -------------------------------- ### Instantiate Bundle Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Constructor for the Bundle Method. Requires an oracle and projection function. Requires 'gurobipy'. Optional parameters include dimension, epsilon, mu, and optimization sense. ```python BundleMethod(oracle, projection_function, dimension=0, epsilon=0.01, mu=0.5, sense='min') ``` -------------------------------- ### DualMethodsFactory for Instantiating Optimization Methods Source: https://context7.com/robin-vjc/nsopy/llms.txt Utilizes DualMethodsFactory to create optimization method instances with a standardized interface. Demonstrates listing available methods and configuring a specific method ('DSA') with parameters. Requires a problem class adhering to a standard interface. ```python import numpy as np from nsopy.methods_factory import DualMethodsFactory, AVAILABLE_METHODS from nsopy.loggers import EnhancedDualMethodLogger # Define a problem class with standard interface class MyProblem: def __init__(self): self.dimension = 2 def oracle(self, lambda_k): c = np.array([-0.5 - 0.5*lambda_k[0] + lambda_k[1], -1 - 0.5*lambda_k[0] + lambda_k[1], 1 - lambda_k[0]]) x_k = (c < 0).astype(float) diff_d_k = np.array([1 - 0.5*x_k[0] - 0.5*x_k[1] - x_k[2], x_k[0] + x_k[1] - 1]) d_k = np.dot(c, x_k) + lambda_k[0] - lambda_k[1] return x_k, d_k, diff_d_k def projection_function(self, lambda_k): return np.maximum(lambda_k, 0) problem = MyProblem() # List available methods print(f"Available methods: {AVAILABLE_METHODS}") # ('SG 1/k', 'SG const', 'UPGM', 'UDGM', 'UFGM', 'DSA', 'TA 1', 'TA 2', 'CP', 'bundle') # Create method using factory method = DualMethodsFactory(problem, method='DSA', param=1.0) logger = EnhancedDualMethodLogger(method) for _ in range(50): method.step() print(f"Result: {logger.lambda_k_iterates[-1]}") print(f"Value: {logger.d_k_iterates[-1]}") ``` -------------------------------- ### Solve Optimization Problem with Subgradient Method Source: https://github.com/robin-vjc/nsopy/blob/master/docs/index.md Initialize and run the SubgradientMethod from nsopy. Requires the oracle function, projection function, and optimization parameters like dimension and stepsize rule. ```python from nsopy.methods.subgradient import SubgradientMethod from nsopy.loggers import GenericMethodLogger method = SubgradientMethod(oracle, projection_function, dimension=1, stepsize_0=0.1, stepsize_rule='constant', sense='min') logger = GenericMethodLogger(method) for iteration in range(200): method.step() ``` -------------------------------- ### Instantiate Universal Fast Gradient Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Constructor for the Universal Fast Gradient Method (UFGM). Requires an oracle and projection function. Optional parameters include dimension, epsilon, averaging flag, and optimization sense. ```python UniversalFGM(oracle, projection_function, dimension=0, epsilon=1.0, averaging=False, sense='min') ``` -------------------------------- ### Import Optimization Methods Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/01. Analytical Example.ipynb Imports various optimization methods from the nsopy library, including quasi-monotone and universal methods. ```python from nsopy.methods.quasi_monotone import SGMDoubleSimpleAveraging as DSA from nsopy.methods.quasi_monotone import SGMTripleAveraging as TA from nsopy.methods.subgradient import SubgradientMethod as SG from nsopy.methods.universal import UniversalPGM as UPGM from nsopy.methods.universal import UniversalDGM as UDGM from nsopy.methods.universal import UniversalFGM as UFGM from nsopy.loggers import GenericDualMethodLogger ``` -------------------------------- ### Instantiate Standard Subgradient Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Constructor for the Standard Subgradient Method. Requires an oracle and projection function. Optional parameters include dimension, initial stepsize, stepsize rule, and optimization sense. ```python SubgradientMethod(oracle, projection_function, dimension=0, stepsize_0=1.0, stepsize_rule='1/k', sense='min') ``` -------------------------------- ### UniversalPGM Implementation Source: https://context7.com/robin-vjc/nsopy/llms.txt Sets up and runs the Universal Primal Gradient Method. Requires defining an oracle function and a projection function. The accuracy is controlled by the epsilon parameter. ```python import numpy as np from nsopy.methods.universal import UniversalPGM from nsopy.loggers import EnhancedDualMethodLogger def oracle(x_k): fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) diff_fi = [-2, -1.0/3.0, 1] max_i = fi_x_k.index(f_x_k) diff_f_xk = np.array([diff_fi[max_i]]) return 0, f_x_k, diff_f_xk def projection_function(x_k): return np.maximum(x_k, 0) method = UniversalPGM( oracle=oracle, projection_function=projection_function, dimension=1, epsilon=0.1, # Accuracy parameter for line search averaging=False, # Use averaged iterates (theoretical guarantees) sense='min' ) logger = EnhancedDualMethodLogger(method) for _ in range(50): method.step() print(f"Solution: {logger.lambda_k_iterates[-1]}") print(f"Value: {logger.d_k_iterates[-1]}") print(f"Oracle calls: {logger.oracle_calls[-1]}") print(f"Computation time: {logger.iteration_time[-1]:.4f}s") ``` -------------------------------- ### Instantiate Universal Dual Gradient Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Constructor for the Universal Dual Gradient Method (UDGM). Requires an oracle and projection function. Optional parameters include dimension, epsilon, averaging flag, and optimization sense. ```python UniversalDGM(oracle, projection_function, dimension=0, epsilon=1.0, averaging=False, sense='min') ``` -------------------------------- ### Instantiate Cutting Planes Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Constructor for the Cutting Planes Method. Requires an oracle and projection function. Requires 'gurobipy'. Optional parameters include dimension, epsilon (stopping criterion), search box min/max, and optimization sense. ```python CuttingPlanesMethod(oracle, projection_function, dimension=0, epsilon=0.01, search_box_min=-10, search_box_max=10, sense='min') ``` -------------------------------- ### Instantiate Universal Primal Gradient Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Constructor for the Universal Primal Gradient Method (UPGM). Requires an oracle and projection function. Optional parameters include dimension, epsilon, averaging flag, and optimization sense. ```python UniversalPGM(oracle, projection_function, dimension=0, epsilon=1.0, averaging=False, sense='min') ``` -------------------------------- ### Run UniversalPGM optimization steps Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Executes the dual step of the UniversalPGM optimizer for a fixed number of iterations to find the optimal dual solution. ```python for iteration in range(60): optimizer.dual_step() ``` -------------------------------- ### Inspect Solution Iterates Source: https://github.com/robin-vjc/nsopy/blob/master/docs/index.md Print the last few iterates of the solution found by the optimization method to inspect convergence. ```python print(logger.x_k_iterates[-5:]) ``` -------------------------------- ### Instantiate SGM Double Simple Averaging Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Constructor for the SGM Double Simple Averaging method. Requires an oracle and projection function. Optional parameters include dimension, gamma, and optimization sense. ```python SGMDoubleSimpleAveraging(oracle, projection_function, dimension=0, gamma=1.0, sense='min') ``` -------------------------------- ### UniversalDGM Implementation Source: https://context7.com/robin-vjc/nsopy/llms.txt Configures and executes the Universal Dual Gradient Method. This method operates in the dual space and can be beneficial for specific problem structures. Averaging can be enabled for theoretical guarantees. ```python import numpy as np from nsopy.methods.universal import UniversalDGM from nsopy.loggers import EnhancedDualMethodLogger def oracle(x_k): fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) diff_fi = [-2, -1.0/3.0, 1] max_i = fi_x_k.index(f_x_k) diff_f_xk = np.array([diff_fi[max_i]]) return 0, f_x_k, diff_f_xk def projection_function(x_k): return np.maximum(x_k, 0) method = UniversalDGM( oracle=oracle, projection_function=projection_function, dimension=1, epsilon=0.5, averaging=True, # Enable averaging for theoretical guarantees sense='min' ) logger = EnhancedDualMethodLogger(method) for _ in range(30): method.step() print(f"Final solution: {logger.lambda_k_iterates[-1]}") print(f"Final value: {logger.d_k_iterates[-1]}") ``` -------------------------------- ### SubgradientMethod for Non-Smooth Convex Optimization Source: https://context7.com/robin-vjc/nsopy/llms.txt Implements the standard subgradient method. Supports 'constant', '1/k', and '1/sqrt(k)' stepsize rules. Requires an oracle and a projection function for the feasible set. ```python import numpy as np from nsopy.methods.subgradient import SubgradientMethod from nsopy.loggers import GenericMethodLogger # Define the oracle for a piecewise linear function: max(-2x+2, -x/3+1, x-2) def oracle(x_k): fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) # function value diff_fi = [-2, -1.0/3.0, 1] # gradients of components max_i = fi_x_k.index(f_x_k) diff_f_xk = np.array([diff_fi[max_i]]) # subgradient return 0, f_x_k, diff_f_xk # Projection onto non-negative reals (X = R+) def projection_function(x_k): return np.maximum(x_k, 0) # Initialize method with constant stepsize method = SubgradientMethod( oracle=oracle, projection_function=projection_function, dimension=1, stepsize_0=0.1, stepsize_rule='constant', # Options: 'constant', '1/k', '1/sqrt(k)' sense='min' ) # Attach logger to track iterates logger = GenericMethodLogger(method) # Run optimization for _ in range(200): method.step() # Access results print(f"Final iterate: {logger.x_k_iterates[-1]}") # ~2.25 (optimal x*) print(f"Final value: {logger.f_k_iterates[-1]}") # ~-0.25 (optimal f(x*)) ``` -------------------------------- ### Run Optimization Iterations Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Executes the optimization process for a fixed number of iterations. The dual step is performed in each iteration, and the output shows the current iteration number. ```python for iteration in range(10): print(f"{iteration=}") optimizer.dual_step() # slight buf in the record of first iteration logger.x_k_iterates[0] = problem.oracle([0, 0])[0] ``` -------------------------------- ### Instantiate SGM Triple Averaging Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Constructor for the SGM Triple Averaging method. Requires an oracle and projection function. Optional parameters include dimension, variant, gamma, and optimization sense. Variant can be 1 or 2. ```python SGMTripleAveraging(oracle, projection_function, dimension=0, variant=1, gamma=1.0, sense='min') ``` -------------------------------- ### BundleMethod Optimization Source: https://context7.com/robin-vjc/nsopy/llms.txt Implements the BundleMethod for optimization with proximal regularization. Requires a custom oracle and projection function. Suitable for problems where stable convergence is prioritized over speed. ```python import numpy as np from nsopy.methods.bundle import BundleMethod from nsopy.loggers import GenericDualMethodLogger def oracle(x_k): fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) diff_fi = [-2, -1.0/3.0, 1] max_i = fi_x_k.index(f_x_k) diff_f_xk = np.array([diff_fi[max_i]]) return 0, f_x_k, diff_f_xk def projection_function(x_k): return np.maximum(x_k, 0) method = BundleMethod( oracle=oracle, projection_function=projection_function, dimension=1, epsilon=0.01, # Optimality tolerance mu=0.5, # Proximal weight (regularization) sense='min' ) # Optional: set dual domain constraints # method.set_dual_domain(type='positive orthant') logger = GenericDualMethodLogger(method) for _ in range(20): method.step() if not method.optimizer_not_yet_found: break print(f"Solution: {method.lambda_k}") print(f"Optimal value: {method.d_k}") ``` -------------------------------- ### GenericMethodLogger for Optimization Progress Source: https://context7.com/robin-vjc/nsopy/llms.txt Demonstrates the use of GenericMethodLogger to track optimization iterates (x_k) and function values (f_k). Includes plotting the convergence of function values using matplotlib. Other logger types like EnhancedDualMethodLogger and SlimDualMethodLogger are available for different tracking needs. ```python import numpy as np from nsopy.methods.subgradient import SubgradientMethod from nsopy.loggers import ( GenericMethodLogger, GenericDualMethodLogger, EnhancedDualMethodLogger, SlimDualMethodLogger ) def oracle(x_k): fi_x_k = [-2*x_k + 2, -1.0/3*x_k + 1, x_k - 2] f_x_k = max(fi_x_k) diff_fi = [-2, -1.0/3.0, 1] max_i = fi_x_k.index(f_x_k) diff_f_xk = np.array([diff_fi[max_i]]) return 0, f_x_k, diff_f_xk def projection_function(x_k): return np.maximum(x_k, 0) method = SubgradientMethod(oracle, projection_function, dimension=1, sense='min') # Basic logger - tracks x_k and f_k logger = GenericMethodLogger(method) # Enhanced logger - also tracks time and oracle calls # enhanced_logger = EnhancedDualMethodLogger(method) # Slim logger - only d_k for memory efficiency on large problems # slim_logger = SlimDualMethodLogger(method) for _ in range(100): method.step() # Access logged data print(f"Iterates: {logger.x_k_iterates[-5:]}") print(f"Function values: {logger.f_k_iterates[-5:]}") # Plot convergence import matplotlib.pyplot as plt plt.plot(logger.f_k_iterates) plt.xlabel('Iteration') plt.ylabel('f(x_k)') plt.title('Convergence Plot') plt.show() ``` -------------------------------- ### Universal Gradient Methods Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Implementation of Nesterov's universal gradient methods (primal, dual, and fast versions). ```APIDOC * **Universal Gradient Methods** Implementation of Nesterov's [universal gradient methods](http://link.springer.com/article/10.1007/s10107-014-0790-0), primal, dual and fast versions. ```python UniversalPGM(oracle, projection_function, dimension=0, epsilon=1.0, averaging=False, sense='min') UniversalDGM(oracle, projection_function, dimension=0, epsilon=1.0, averaging=False, sense='min'): UniversalFGM(oracle, projection_function, dimension=0, epsilon=1.0, averaging=False, sense='min'): ``` ``` -------------------------------- ### Quasi-Monotone Methods Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Implementation of double simple averaging, and triple averaging methods for quasi-monotone optimization. ```APIDOC * **Quasi-Monotone Methods** Implementation of double simple averaging, and triple averaging methods from Nesterov's paper on [quasi-monotone methods](http://link.springer.com/article/10.1007/s10957-014-0677-5). ```python SGMDoubleSimpleAveraging(oracle, projection_function, dimension=0, gamma=1.0, sense='min') SGMTripleAveraging(oracle, projection_function, dimension=0, variant=1, gamma=1.0, sense='min'): ``` Variants of `SGMTripleAveraging` available: `variant: [1, 2]` ``` -------------------------------- ### Inspect Earlier Primal Solutions Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/02. Application to Duality.ipynb View a history of primal solutions (x_k) from earlier iterations stored in the method logger. This helps in understanding solution behavior and potential oscillations. ```python method_logger.x_k_iterates[-10:] ``` -------------------------------- ### Cutting Planes Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md The Cutting Planes Method requires the 'gurobipy' library. It uses an epsilon parameter for suboptimality. ```APIDOC * **Cutting Planes Method** *Warning*: this method requires `gurobipy`; if you are an academic, you can get a free license [here](http://www.gurobi.com/academia/for-universities]). ```python CuttingPlanesMethod(oracle, projection_function, dimension=0, epsilon=0.01, search_box_min=-10, search_box_max=10, sense='min') ``` The parameter `epsilon` is the absolute required suboptimality level `|f_k - f*|` used as a stopping criterion. Note that a search box needs to be specified. ``` -------------------------------- ### SGMDoubleSimpleAveraging for Quasi-Monotone Convergence Source: https://context7.com/robin-vjc/nsopy/llms.txt Implements Subgradient Method with Double Simple Averaging for quasi-monotone convergence. Suitable for Lagrangian dual problems. Requires a dual oracle and projection onto the dual feasible set. ```python import numpy as np from nsopy.methods.quasi_monotone import SGMDoubleSimpleAveraging from nsopy.loggers import GenericDualMethodLogger # Define a 2D analytical problem (Lagrangian dual) class DualProblem: def __init__(self): self.dimension = 2 def oracle(self, lambda_k): # Inner problem: min -0.5x1 - x2 + x3 with dualized constraints c = np.zeros(3) x_k = np.zeros(3) c[0] = -0.5 - 0.5*lambda_k[0] + lambda_k[1] c[1] = -1 - 0.5*lambda_k[0] + lambda_k[1] c[2] = 1 - lambda_k[0] x_k[0] = 1 if c[0] < 0 else 0 x_k[1] = 1 if c[1] < 0 else 0 x_k[2] = 1 if c[2] < 0 else 0 diff_d_k = np.array([ 1 - 0.5*x_k[0] - 0.5*x_k[1] - x_k[2], x_k[0] + x_k[1] - 1 ]) d_k = c[0]*x_k[0] + c[1]*x_k[1] + c[2]*x_k[2] + lambda_k[0] - lambda_k[1] return x_k, d_k, diff_d_k def projection_function(self, lambda_k): return np.maximum(lambda_k, 0) # Project onto positive orthant problem = DualProblem() method = SGMDoubleSimpleAveraging( oracle=problem.oracle, projection_function=problem.projection_function, dimension=problem.dimension, gamma=1.0, # Tuning parameter sense='max' # Maximize dual function ) logger = GenericDualMethodLogger(method) for _ in range(50): method.step() print(f"Dual multipliers: {logger.lambda_k_iterates[-1]}") print(f"Dual value: {logger.d_k_iterates[-1]}") # Should approach -0.5 ``` -------------------------------- ### Standard Subgradient Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Details on the Standard Subgradient Method. ```APIDOC ## Available Methods * **Standard Subgradient Method** ```python SubgradientMethod(oracle, projection_function, dimension=0, stepsize_0=1.0, stepsize_rule='1/k', sense='min') ``` Stepsize rules valiable: `stepsize_rule: ['constant', '1/k', '1/sqrt(k)']` ``` -------------------------------- ### Bundle Method Source: https://github.com/robin-vjc/nsopy/blob/master/README.md A basic variant of the Bundle Method, also requiring 'gurobipy'. ```APIDOC * **Bundle Method** *Warning*: this method requires `gurobipy`; if you are an academic, you can get a free license [here](http://www.gurobi.com/academia/for-universities]). Implementation of a basic variant of the bundle method. ```python BundleMethod(oracle, projection_function, dimension=0, epsilon=0.01, mu=0.5, sense='min'): ``` ``` -------------------------------- ### Inspect last dual iterates Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Displays the last five dual iterates found by the optimizer, representing approximate solutions to the dual problem. ```python logger.lambda_k_iterates[-5:] ``` -------------------------------- ### Run Optimization Method Until Convergence Source: https://context7.com/robin-vjc/nsopy/llms.txt Iteratively calls the step method of an optimization object until convergence criteria are met or a maximum number of iterations is reached. Prints the final solution and optimal value. ```python for _ in range(20): method.step() if not method.optimizer_not_yet_found: break print(f"Solution: {method.lambda_k}") print(f"Optimal value: {method.d_k}") ``` -------------------------------- ### Important Remarks Source: https://github.com/robin-vjc/nsopy/blob/master/README.md Key considerations for using the nsopy library, including dimension handling, projection functions, and performance. ```APIDOC ## Important Remarks * Methods have to either be instantiated with the appropriate dimension argument, or implement a special case for 0. The basic usage example above illustrates an oracle implementing such a special case. For this example, alternatively one could have instantiated the solution method with `dimension = 1`. * The first-order oracle must also provide a projection function; [here is a list of cases](docs/img/simple_projections.png) for which the projection operation is computationally inexpensive. * Currently, all methods are implemented in Python. Numerical performance is not optimized, but they may be still useful for quick comparisons or for applications in which the main computational burden is in evaluating the first order oracle. ``` -------------------------------- ### OptimalShedding class for resource allocation Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Defines a class for optimal shedding problems, modeling customer demand and product availability. It uses OR-Tools for solving subproblems. ```python class OptimalShedding: def __init__(self, n_customers: int = 10, n_products: int = 2): self.n_customers = n_customers self.n_products = n_products low_demand = 2 high_demand = 10 mean_demand = low_demand + (high_demand - low_demand)/2 low_reward = 8 high_reward = 12 # model data self.R_i = np.random.randint(low_reward, high_reward, n_customers) self.P_ij = np.random.randint(low_reward*n_customers, high_reward*n_customers, (n_customers, n_products)) self.M_ij = np.random.randint(low_demand, low_demand+2, (n_customers, n_products)) self.D_ij = np.random.randint(low_demand, high_demand, (n_customers, n_products)) self.C_j = mean_demand*0.5*np.random.randint(int(n_customers*1/3), int(n_customers*2/3), n_products) def solve_subproblem(self, i: int, lambda_k: np.ndarray): solver = pywraplp.Solver.CreateSolver("SAT") # add variables x_i = None y_ij = [None for _ in range(self.n_products)] x_i = solver.BoolVar(f"x_{i}") for j in range(self.n_products): y_ij[j] = solver.NumVar(0.0, float(self.D_ij[i, j]), f"D_({i},{j})") # add constraints for j in range(self.n_products): solver.Add(self.M_ij[i, j]*x_i <= y_ij[j], f"Min_qty_({i}, {j})") solver.Add(y_ij[j] <= self.D_ij[i, j]*x_i, f"Demand_({i}, {j})") # objective solver.Minimize(-self.R_i[i]*x_i + sum([(lambda_k[j] - (self.P_ij[i,j]/(self.D_ij[i,j])))*y_ij[j] for j in range(self.n_products)])) # solve status = solver.Solve() if status != pywraplp.Solver.OPTIMAL: raise ValueError(f"Could not solve model: {status}") # return values x_ik = np.array([x_i.solution_value()] + [y.solution_value() for y in y_ij]) d_ik = solver.Objective().Value() ``` -------------------------------- ### Inspect last primal iterates Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Displays the last five primal iterates, which are approximate solutions to the original primal optimization problem. ```python logger.x_k_iterates[-5:] ``` -------------------------------- ### Import necessary libraries for NSopy Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Imports essential libraries for numerical operations, plotting, and NSopy's optimization modules. Sets a random seed for reproducibility and enables interactive matplotlib plots. ```python import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np from ortools.linear_solver import pywraplp from nsopy.methods.universal import UniversalPGM, UniversalDGM, UniversalFGM from nsopy.methods.subgradient import SubgradientMethod from nsopy.loggers import GenericDualMethodLogger np.random.seed(102) %matplotlib notebook ``` -------------------------------- ### Visualize dual space trajectory Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Plots the trajectory of dual iterates in 3D space, showing the dual objective function surface and the path taken by the optimizer. ```python box = np.linspace(0, 3, 30) fig = plt.figure() ax = fig.add_subplot(111, projection='3d') ax.plot_trisurf(np.array([lmd_1 for lmd_1 in box for lmd_2 in box]), np.array([lmd_2 for lmd_1 in box for lmd_2 in box]), np.array([problem.oracle(np.array([lmd_1, lmd_2]))[1] for lmd_1 in box for lmd_2 in box]), alpha=0.7) plt.plot([lmd[0] for lmd in logger.lambda_k_iterates], [lmd[1] for lmd in logger.lambda_k_iterates], [d_lmd for d_lmd in logger.d_k_iterates], 'r.-') ax.set_xlabel('$λ_1$') ax.set_ylabel('$λ_2$') ax.set_zlabel('$d(λ)$') ``` -------------------------------- ### Analyze Customer Service and Shedding Source: https://github.com/robin-vjc/nsopy/blob/master/notebooks/03. Dual Decomposition.ipynb Analyzes the last primal iterate to determine which customers are served and how. It identifies customers that are not served (shed) and prints details about the service units and demands for served customers. ```python x_sol = logger.x_k_iterates[-1] not_served = [] for i in range(problem.n_customers): if x_sol[i,0] == 0: not_served.append(i) print(f'We have {problem.n_customers} customers.') print(f'We shed customers {not_served}.') for i in range(problem.n_customers): if i not in not_served: print(f'Customer {i} is served {x_sol[i,1]} "flop" units (min: {problem.M_ij[i,0]}, demand: {problem.D_ij[i,0]}) and {x_sol[i,2]} "network" units (min: {problem.M_ij[i,1]}, demand: {problem.D_ij[i,1]}) ') ```