├── tests ├── __init__.py └── lava │ ├── __init__.py │ ├── tutorials │ └── __init__.py │ └── lib │ └── optimization │ ├── __init__.py │ ├── apps │ ├── __init__.py │ ├── tsp │ │ ├── __init__.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ └── test_q_matrix_generator.py │ │ ├── test_problems.py │ │ └── test_solver.py │ ├── schduler │ │ ├── __init__.py │ │ ├── test_problems.py │ │ └── test_solver.py │ └── clustering │ │ ├── __init__.py │ │ ├── utils │ │ ├── __init__.py │ │ └── test_q_matrix_generator.py │ │ ├── test_problems.py │ │ └── test_solver.py │ ├── problems │ ├── __init__.py │ ├── bayesian │ │ ├── __init__.py │ │ ├── test_processes.py │ │ └── test_models.py │ ├── test_coefficients.py │ ├── test_cost.py │ └── test_variables.py │ ├── solvers │ ├── __init__.py │ ├── lca │ │ └── __init__.py │ ├── bayesian │ │ └── __init__.py │ └── generic │ │ ├── __init__.py │ │ ├── qp │ │ └── __init__.py │ │ ├── nebm │ │ ├── __init__.py │ │ └── test_models.py │ │ ├── scif │ │ ├── __init__.py │ │ └── test_process.py │ │ ├── solution_reader │ │ ├── __init__.py │ │ └── test_solution_reader.py │ │ ├── monitoring_processes │ │ ├── __init__.py │ │ └── solution_readout │ │ │ ├── __init__.py │ │ │ └── test_solution_readout.py │ │ ├── data │ │ └── qp │ │ │ └── ex_qp_small.npz │ │ ├── solution_finder │ │ └── test_solution_finder.py │ │ └── test_solver_cpu_backend_parallel.py │ └── utils │ ├── generators │ ├── test_mis.py │ └── test_clustering_tsp_vrp.py │ ├── test_report_analyzer.py │ └── test_solver_tuner.py ├── src └── lava │ └── lib │ └── optimization │ ├── README.md │ ├── solvers │ ├── generic │ │ ├── sconfig.py │ │ ├── processes.py │ │ ├── solution_finder │ │ │ └── process.py │ │ ├── solution_reader │ │ │ ├── process.py │ │ │ └── models.py │ │ ├── monitoring_processes │ │ │ └── solution_readout │ │ │ │ ├── process.py │ │ │ │ └── models.py │ │ ├── cost_integrator │ │ │ ├── models.py │ │ │ └── process.py │ │ ├── read_gate │ │ │ └── process.py │ │ ├── dataclasses.py │ │ ├── qp │ │ │ └── solver.py │ │ ├── nebm │ │ │ └── models.py │ │ ├── annealing │ │ │ └── process.py │ │ └── scif │ │ │ └── process.py │ ├── lca │ │ ├── residual_neuron │ │ │ ├── process.py │ │ │ └── models.py │ │ ├── util.py │ │ ├── v1_neuron │ │ │ ├── process.py │ │ │ └── models.py │ │ ├── models.py │ │ └── process.py │ └── bayesian │ │ ├── processes.py │ │ └── models.py │ ├── utils │ └── datatype_converter.py │ ├── problems │ ├── cost.py │ ├── bayesian │ │ ├── processes.py │ │ └── models.py │ ├── coefficients.py │ └── variables.py │ └── apps │ ├── tsp │ ├── problems.py │ └── solver.py │ └── clustering │ └── problems.py ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── issues.yml │ └── ci.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── tutorials ├── SatSchDemoSchematic.png ├── data │ ├── qp │ │ └── ex_qp_small.npz │ └── qubo │ │ └── workloads │ │ └── mis-uniform │ │ └── gen_mis_uniform.ipynb └── mnist_pretrained_dictionary.npy ├── __init__.py ├── LICENSE ├── .gitignore └── pyproject.toml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/tutorials/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/tsp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/problems/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/schduler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/lca/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/clustering/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/tsp/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/problems/bayesian/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/bayesian/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/qp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @lava-nc/lava-optimization-committers 2 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/clustering/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/nebm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/scif/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/sconfig.py: -------------------------------------------------------------------------------- 1 | num_in_ports = 1 2 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/solution_reader/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/monitoring_processes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tutorials/SatSchDemoSchematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lava-nc/lava-optimization/HEAD/tutorials/SatSchDemoSchematic.png -------------------------------------------------------------------------------- /tutorials/data/qp/ex_qp_small.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lava-nc/lava-optimization/HEAD/tutorials/data/qp/ex_qp_small.npz -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | -------------------------------------------------------------------------------- /tutorials/mnist_pretrained_dictionary.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lava-nc/lava-optimization/HEAD/tutorials/mnist_pretrained_dictionary.npy -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/data/qp/ex_qp_small.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lava-nc/lava-optimization/HEAD/tests/lava/lib/optimization/solvers/generic/data/qp/ex_qp_small.npz -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/monitoring_processes/solution_readout/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Lava Developer Guide 4 | url: https://lava-nc.org/developer_guide.html#how-to-contribute-to-lava 5 | about: How to contribute to Lava. 6 | - name: Lava Community Support 7 | url: https://github.com/lava-nc/lava-optimization/discussions 8 | about: Please ask and answer questions here. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 1-feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### User story 11 | As a user, I want to [capability] to [benefit]. 12 | 13 | ### Conditions of satisfaction 14 | - An optional list of conditions that have to be fulfilled for this feature to be complete. 15 | - For example: "Users can add both individual items and lists as parameters." 16 | 17 | ### Acceptance tests 18 | - An optional list of tests that should be written to automatically test the new feature. 19 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to the NCL planning project and label them 2 | permissions: {} 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | - transferred 9 | 10 | jobs: 11 | add-to-project: 12 | name: Add issue to project 13 | permissions: {} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/add-to-project@v0.4.0 17 | with: 18 | project-url: https://github.com/orgs/lava-nc/projects/3 19 | github-token: ${{ secrets.ADD_ISSUES_TO_PROJECT }} 20 | 21 | label_issues: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | issues: write 25 | steps: 26 | - uses: actions/github-script@v6 27 | with: 28 | script: | 29 | github.rest.issues.addLabels({ 30 | issue_number: context.issue.number, 31 | owner: context.repo.owner, 32 | repo: context.repo.repo, 33 | labels: ["0-needs-review"] 34 | }) 35 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/tsp/test_problems.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | from lava.lib.optimization.apps.tsp.problems import TravellingSalesmanProblem 4 | from lava.lib.optimization.utils.generators.clustering_tsp_vrp import ( 5 | UniformlySampledTSP) 6 | 7 | 8 | class TestTravellingSalesmanProblem(unittest.TestCase): 9 | def setUp(self) -> None: 10 | np.random.seed(2) 11 | self.utsp = UniformlySampledTSP(num_starting_pts=1, 12 | num_dest_nodes=5, 13 | domain=[(0, 0), (25, 25)]) 14 | self.tsp = TravellingSalesmanProblem( 15 | waypt_coords=self.utsp.dest_coords, 16 | starting_pt=self.utsp.starting_coords[0] 17 | ) 18 | 19 | def test_init(self): 20 | self.assertIsInstance(self.tsp, TravellingSalesmanProblem) 21 | 22 | def test_properties(self): 23 | self.assertEqual(self.tsp.num_waypts, 5) 24 | self.assertListEqual(self.tsp.waypt_ids, list(range(2, 7))) 25 | 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/utils/datatype_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import numpy as np 5 | 6 | 7 | def convert_to_fp(matrix, mantissa_bits): 8 | """Function that returns the exponent, mantissa representation for 9 | floating point numbers that need to be represented on Loihi. A global exp 10 | is calculated for the matrices based on the max absolute value in the 11 | matrix. This is then used to calculate the manstissae in the matrix. 12 | 13 | Args: 14 | matrix (np.float): The input floating point matrix that needs to be 15 | converted 16 | mantissa_bits (int): number of bits that the mantissa uses for it's 17 | representation (Including sign) 18 | 19 | Returns: 20 | mat_fp_man (np.int): The matrix in 21 | """ 22 | if np.linalg.norm(matrix) == 0: 23 | return matrix.astype(int), 0 24 | else: 25 | exp = np.ceil(np.log2(np.max(np.abs(matrix)))) - mantissa_bits + 1 26 | mat_fp_man = (matrix // 2**exp).astype(int) 27 | return mat_fp_man.astype(int), exp.astype(int) 28 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/clustering/test_problems.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | from lava.lib.optimization.apps.clustering.problems import ClusteringProblem 4 | from lava.lib.optimization.utils.generators.clustering_tsp_vrp import ( 5 | GaussianSampledClusteringProblem) 6 | 7 | 8 | class TestClusteringProblem(unittest.TestCase): 9 | def setUp(self) -> None: 10 | np.random.seed(2) 11 | self.gcp = GaussianSampledClusteringProblem(num_clusters=4, 12 | num_points=10, 13 | domain=[(0, 0), (25, 25)], 14 | variance=3) 15 | self.cp = ClusteringProblem(point_coords=self.gcp.point_coords, 16 | center_coords=self.gcp.center_coords) 17 | 18 | def test_init(self): 19 | self.assertIsInstance(self.cp, ClusteringProblem) 20 | 21 | def test_properties(self): 22 | self.assertEqual(self.cp.num_clusters, 4) 23 | self.assertEqual(self.cp.num_points, 10) 24 | self.assertListEqual(self.cp.cluster_ids, list(range(1, 5))) 25 | self.assertListEqual(self.cp.point_ids, list(range(5, 15))) 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/scif/test_process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import unittest 5 | import numpy as np 6 | from lava.lib.optimization.solvers.generic.scif.process import CspScif, QuboScif 7 | 8 | 9 | class TestCspScifProcess(unittest.TestCase): 10 | """Tests for CspScif class""" 11 | 12 | def test_init(self) -> None: 13 | """Tests instantiation of CspScif""" 14 | scif = CspScif(shape=(10,), step_size=2, theta=8, sustained_on_tau=-10) 15 | 16 | self.assertEqual(scif.shape, (10,)) 17 | self.assertEqual(scif.step_size.init, 2) 18 | self.assertEqual(scif.theta.init, 8) 19 | self.assertEqual(scif.sustained_on_tau.init, -10) 20 | 21 | 22 | class TestQuboScifProcess(unittest.TestCase): 23 | """Tests for QuboScif class""" 24 | 25 | def test_init(self) -> None: 26 | """Tests instantiation of QuboScif""" 27 | scif = QuboScif(shape=(10,), theta=8, cost_diag=np.arange(1, 11)) 28 | 29 | self.assertEqual(scif.shape, (10,)) 30 | self.assertEqual(scif.theta.init, 8) 31 | self.assertTrue(np.all(scif.cost_diagonal.init == np.arange(1, 11))) 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/lca/residual_neuron/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Battelle Memorial Institute 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import typing as ty 6 | 7 | import numpy as np 8 | from lava.magma.core.process.ports.ports import InPort, OutPort 9 | from lava.magma.core.process.process import AbstractProcess 10 | from lava.magma.core.process.variable import Var 11 | 12 | 13 | class ResidualNeuron(AbstractProcess): 14 | """Accumulates all input and bias into voltage until it exceeds 15 | spike_height, then fires and resets. 16 | 17 | Parameters 18 | ---------- 19 | spike_height: the threshold to fire and reset at 20 | bias: added to voltage every timestep 21 | """ 22 | 23 | def __init__(self, 24 | spike_height: float, 25 | bias: ty.Union[int, np.ndarray], 26 | shape: ty.Optional[tuple] = (1,), 27 | **kwargs) -> None: 28 | super().__init__(shape=shape, 29 | spike_height=spike_height, 30 | **kwargs) 31 | 32 | self.spike_height = Var(shape=(1,), init=spike_height) 33 | self.a_in = InPort(shape=shape) 34 | self.s_out = OutPort(shape=shape) 35 | self.v = Var(shape=shape) 36 | self.bias = Var(shape=shape, init=bias) 37 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/lca/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Battelle Memorial Institute 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | 7 | 8 | def sign_extend_24bit(x): 9 | """ 10 | Sign extends a signed 24-bit numpy array into a signed 32-bit array. 11 | """ 12 | x = x.astype(np.int32) 13 | mask = (np.right_shift(x, 23) > 0) * np.array([0xFF000000], dtype=np.int32) 14 | return np.bitwise_or(x, mask) 15 | 16 | 17 | def apply_activation(voltage, threshold): 18 | """ 19 | Applies a soft-threshold activation 20 | """ 21 | return np.maximum(np.abs(voltage) - threshold, 0) * np.sign(voltage) 22 | 23 | 24 | def get_1_layer_weights(dictionary, tau): 25 | """ 26 | Returns the floating-pt weights for 1 Layer LCA. 27 | """ 28 | return ((dictionary @ -dictionary.T) + np.eye(dictionary.shape[0])) * tau 29 | 30 | 31 | def get_1_layer_bias(dictionary, tau, input): 32 | """ 33 | Returns the floating-pt bias for 1 Layer LCA. 34 | """ 35 | return (input @ dictionary.T) * tau 36 | 37 | 38 | def get_fixed_pt_scale(sparse_coding): 39 | """ 40 | Returns the optimal scale factor given a known or estimated sparse coding. 41 | The scale is the largest power of 2 such that the sparse_coding * scale does 42 | not exceed 2**24 43 | """ 44 | return 2 ** (24 - np.ceil(np.log2(np.max(np.abs(sparse_coding))))) 45 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/processes.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import typing as ty 5 | 6 | import numpy as np 7 | from lava.lib.optimization.problems.coefficients import CoefficientTensorsMixin 8 | from lava.magma.core.process.interfaces import AbstractProcessMember 9 | from lava.magma.core.process.ports.ports import InPort 10 | from lava.magma.core.process.variable import Var 11 | 12 | 13 | def _vars_from_coefficients( 14 | coefficients: CoefficientTensorsMixin, 15 | ) -> ty.Dict[int, AbstractProcessMember]: 16 | vars = dict() 17 | for rank, coeff in coefficients.items(): 18 | if rank == 1: 19 | init = -coeff 20 | if rank == 2: 21 | linear_component = -coeff.diagonal() 22 | quadratic_component = coeff * np.logical_not(np.eye(*coeff.shape)) 23 | if 1 in vars.keys(): 24 | vars[1].init = vars[1].init + linear_component 25 | else: 26 | vars[1] = Var( 27 | shape=linear_component.shape, init=linear_component 28 | ) 29 | init = -quadratic_component 30 | vars[rank] = Var(shape=coeff.shape, init=init) 31 | return vars 32 | 33 | 34 | def _in_ports_from_coefficients( 35 | coefficients: CoefficientTensorsMixin, 36 | ) -> ty.List[AbstractProcessMember]: 37 | in_ports = [ 38 | InPort(shape=coeff.shape) for coeff in coefficients.coefficients 39 | ] 40 | return in_ports 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Intel Corporation 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 1-bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 19 | 20 | **Describe the bug** 21 | A clear and concise description of what the bug is. 22 | 23 | **To reproduce current behavior** 24 | Steps to reproduce the behavior: 25 | 1. When I run this code (add code or minimum test case) ... 26 | ```python 27 | def my_code(): 28 | pass 29 | ``` 30 | 2. I get this error ... 31 | ``` 32 | Error... 33 | ``` 34 | 35 | **Expected behavior** 36 | A clear and concise description of what you expected to happen. 37 | 38 | **Screenshots** 39 | If applicable, add screenshots to help explain your problem. Remove section otherwise. 40 | 41 | **Environment (please complete the following information):** 42 | - Device: [e.g. Laptop, Intel cloud] 43 | - OS: [e.g. Linux] 44 | - Lava version [e.g. 0.6.1] 45 | 46 | **Additional context** 47 | Add any other context about the problem here. Remove section otherwise. 48 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/solution_finder/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import typing as ty 5 | 6 | import numpy as np 7 | from lava.magma.core.process.ports.ports import OutPort 8 | from lava.magma.core.process.process import AbstractProcess, LogConfig 9 | from lava.magma.core.process.variable import Var 10 | 11 | 12 | class SolutionFinder(AbstractProcess): 13 | def __init__( 14 | self, 15 | cost_diagonal, 16 | cost_coefficients, 17 | constraints, 18 | backend, 19 | hyperparameters, 20 | discrete_var_shape, 21 | continuous_var_shape, 22 | problem, 23 | name: ty.Optional[str] = None, 24 | log_config: ty.Optional[LogConfig] = None, 25 | ): 26 | super().__init__( 27 | cost_diagonal=cost_diagonal, 28 | cost_coefficients=cost_coefficients, 29 | constraints=constraints, 30 | backend=backend, 31 | hyperparameters=hyperparameters, 32 | discrete_var_shape=discrete_var_shape, 33 | continuous_var_shape=continuous_var_shape, 34 | problem=problem, 35 | name=name, 36 | log_config=log_config, 37 | ) 38 | self.variables_assignment = Var( 39 | shape=discrete_var_shape or continuous_var_shape, init=(1,) 40 | ) 41 | self.cost_last_bytes = Var(shape=(1,), init=(0,)) 42 | self.cost_first_byte = Var(shape=(1,), init=(0,)) 43 | self.cost_out_last_bytes = OutPort(shape=(1,)) 44 | self.cost_out_first_byte = OutPort(shape=(1,)) 45 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/solution_reader/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import typing as ty 5 | 6 | from lava.magma.core.process.ports.ports import InPort, RefPort 7 | from lava.magma.core.process.process import AbstractProcess, LogConfig 8 | from lava.magma.core.process.variable import Var 9 | 10 | 11 | class SolutionReader(AbstractProcess): 12 | def __init__( 13 | self, 14 | var_shape, 15 | target_cost, 16 | min_cost: int = (1 << 31) - 1, 17 | num_in_ports: int = 1, 18 | time_steps_per_algorithmic_step: int = 1, 19 | name: ty.Optional[str] = None, 20 | log_config: ty.Optional[LogConfig] = None, 21 | ): 22 | super().__init__( 23 | var_shape=var_shape, 24 | target_cost=target_cost, 25 | num_in_ports=num_in_ports, 26 | time_steps_per_algorithmic_step=time_steps_per_algorithmic_step, 27 | name=name, 28 | log_config=log_config, 29 | ) 30 | self.solution = Var(shape=var_shape, init=-1) 31 | self.solution_step = Var(shape=(1,), init=-1) 32 | self.min_cost = Var(shape=(2,), init=min_cost) 33 | self.cost = Var(shape=(1,), init=min_cost) 34 | self.satisfaction = Var(shape=(1,), init=0) 35 | self.ref_port = RefPort(shape=var_shape) 36 | for id in range(num_in_ports): 37 | setattr(self, f"read_gate_in_port_first_byte_{id}", 38 | InPort(shape=(1,))) 39 | setattr(self, f"read_gate_in_port_last_bytes_{id}", 40 | InPort(shape=(1,))) 41 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/lca/v1_neuron/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Battelle Memorial Institute 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import typing as ty 6 | 7 | import numpy as np 8 | from lava.magma.core.process.ports.ports import InPort, OutPort 9 | from lava.magma.core.process.process import AbstractProcess 10 | from lava.magma.core.process.variable import Var 11 | 12 | 13 | class V1Neuron(AbstractProcess): 14 | """V1 Neurons used in 1 layer and 2 layer LCA. See corresponding LCA 15 | processes for full dynamics. 16 | 17 | Parameters 18 | ---------- 19 | vth: activation threshold 20 | tau: time constant 21 | bias: bias applied every timestep for 1 layer dynamics 22 | two_layer: If false, use 1 layer dynamics, otherwise use 2 layer dynamics 23 | """ 24 | 25 | def __init__(self, 26 | vth: float, 27 | tau: float, 28 | tau_exp: int, 29 | shape: ty.Optional[tuple] = (1,), 30 | bias: ty.Optional[ty.Union[int, np.ndarray]] = 0, 31 | two_layer: ty.Optional[bool] = True, 32 | **kwargs) -> None: 33 | super().__init__(shape=shape, 34 | vth=vth, 35 | tau=tau, 36 | tau_exp=tau_exp, 37 | two_layer=two_layer, 38 | **kwargs) 39 | 40 | self.vth = Var(shape=(1,), init=vth) 41 | self.tau = Var(shape=(1,), init=tau) 42 | self.tau_exp = Var(shape=(1,), init=tau_exp) 43 | self.a_in = InPort(shape=shape) 44 | self.s_out = OutPort(shape=shape) 45 | self.v = Var(shape=shape) 46 | self.bias = Var(shape=shape, init=bias) 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run CI 2 | permissions: read-all 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | lint: 15 | name: Lint Code 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | lfs: true 22 | 23 | - name: Setup CI 24 | uses: lava-nc/ci-setup-composite-action@v1.5.12_py3.10 25 | with: 26 | repository: 'Lava-Optimization' 27 | 28 | - name: Run flakeheaven (flake8) 29 | run: poetry run flakeheaven lint src/lava tests/ 30 | 31 | security-lint: 32 | name: Security Lint Code 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | with: 38 | lfs: true 39 | 40 | - name: Setup CI 41 | uses: lava-nc/ci-setup-composite-action@v1.5.12_py3.10 42 | with: 43 | repository: 'Lava-Optimization' 44 | 45 | - name: Run bandit 46 | uses: tj-actions/bandit@v5.1 47 | with: 48 | targets: | 49 | src/lava/. 50 | options: "-r --format custom --msg-template '{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}'" 51 | 52 | unit-tests: 53 | name: Unit Test Code + Coverage 54 | runs-on: ${{ matrix.operating-system }} 55 | strategy: 56 | matrix: 57 | operating-system: [ubuntu-latest] 58 | 59 | steps: 60 | - uses: actions/checkout@v3 61 | with: 62 | lfs: true 63 | 64 | - name: Setup CI 65 | uses: lava-nc/ci-setup-composite-action@v1.5.12_py3.10 66 | with: 67 | repository: 'Lava-Optimization' 68 | 69 | - name: Run unit tests 70 | run: poetry run pytest 71 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/problems/cost.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import numpy as np 5 | import typing as ty 6 | 7 | import numpy.typing as npt 8 | from lava.lib.optimization.problems.coefficients import CoefficientTensorsMixin 9 | 10 | CTType = ty.Union[ty.List, npt.ArrayLike] 11 | 12 | 13 | class Cost(CoefficientTensorsMixin): 14 | """Cost function of an optimization problem. 15 | 16 | :param coefficients: cost tensor coefficients. 17 | :param augmented_terms: Tuple of terms not originally defined in the cost 18 | function, e.g. a regularization term or those incorporating constraints 19 | into the cost. Tuple elements have the same type as coefficients. 20 | """ 21 | 22 | def __init__( 23 | self, 24 | *coefficients: CTType, 25 | augmented_terms: ty.Tuple[CTType, ...] = None, 26 | ): 27 | super().__init__(*coefficients) 28 | self._augmented_terms = augmented_terms 29 | 30 | @property 31 | def augmented_terms(self): 32 | """Augmented terms present in the cost function.""" 33 | if self._augmented_terms is None: 34 | return None 35 | else: 36 | at = CoefficientTensorsMixin(*self._augmented_terms) 37 | return at.coefficients 38 | 39 | @augmented_terms.setter 40 | def augmented_terms(self, value): 41 | self._augmented_terms = value 42 | 43 | @property 44 | def is_augmented(self): 45 | """Whether augmented terms are present in the cost function.""" 46 | if self.augmented_terms is None: 47 | return False 48 | return True 49 | 50 | def __call__(self, x: np.ndarray) -> np.ndarray: 51 | """Evaluate the cost at the given solution.""" 52 | return super().evaluate(x) 53 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/lca/residual_neuron/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Battelle Memorial Institute 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | from numbers import Number 6 | import numpy as np 7 | from lava.magma.core.decorator import implements, requires, tag 8 | from lava.magma.core.model.py.model import PyLoihiProcessModel 9 | from lava.magma.core.model.py.ports import PyInPort, PyOutPort 10 | from lava.magma.core.model.py.type import LavaPyType 11 | from lava.magma.core.resources import CPU 12 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 13 | 14 | from lava.lib.optimization.solvers.lca.residual_neuron.process import \ 15 | ResidualNeuron 16 | 17 | 18 | class AbstractPyResidual(PyLoihiProcessModel): 19 | spike_height: Number 20 | a_in: PyInPort 21 | s_out: PyOutPort 22 | v: np.ndarray 23 | bias: np.ndarray 24 | 25 | def run_spk(self): 26 | self.v += self.a_in.recv() + self.bias 27 | activation = np.abs(self.v) > self.spike_height 28 | self.s_out.send(np.where(activation, self.v, 0)) 29 | self.v[activation] = 0 30 | 31 | 32 | @implements(proc=ResidualNeuron, protocol=LoihiProtocol) 33 | @requires(CPU) 34 | @tag('floating_pt') 35 | class PyResidualFloat(AbstractPyResidual): 36 | spike_height: float = LavaPyType(float, float) 37 | a_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, float) 38 | s_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, float) 39 | v: np.ndarray = LavaPyType(np.ndarray, float) 40 | bias: np.ndarray = LavaPyType(np.ndarray, float) 41 | 42 | 43 | @implements(proc=ResidualNeuron, protocol=LoihiProtocol) 44 | @requires(CPU) 45 | @tag('fixed_pt') 46 | class PyResidualFixed(AbstractPyResidual): 47 | spike_height: int = LavaPyType(int, int) 48 | a_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, int) 49 | s_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, int) 50 | v: np.ndarray = LavaPyType(np.ndarray, int) 51 | bias: np.ndarray = LavaPyType(np.ndarray, int) 52 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/problems/bayesian/processes.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: LGPL-2.1-or-later 3 | # See: https://spdx.org/licenses/ 4 | 5 | from lava.magma.core.process.ports.ports import InPort, OutPort 6 | from lava.magma.core.process.process import AbstractProcess 7 | from lava.magma.core.process.variable import Var 8 | 9 | 10 | class BaseObjectiveFunction(AbstractProcess): 11 | """ 12 | A base objective function process that shall be used as the basis of 13 | all black-box processes. 14 | """ 15 | 16 | def __init__(self, num_params: int, num_objectives: int, 17 | **kwargs) -> None: 18 | """initialize the BaseObjectiveFunction 19 | 20 | Parameters 21 | ---------- 22 | num_params : int 23 | an integer specifying the number of parameters within the 24 | search space 25 | num_objectives : int 26 | an integer specifying the number of qualitative attributes 27 | used to measure the black-box function 28 | """ 29 | super().__init__(**kwargs) 30 | 31 | # Internal State Variables 32 | self.num_params = Var((1,), init=num_params) 33 | self.num_objectives = Var((1,), init=num_objectives) 34 | 35 | # Input/Output Ports 36 | self.x_in = InPort((num_params, 1)) 37 | self.y_out = OutPort(((num_params + num_objectives), 1)) 38 | 39 | 40 | class SingleInputFunction(BaseObjectiveFunction): 41 | """ 42 | An abstract process representing a single input/output test function. 43 | """ 44 | 45 | def __init__(self, **kwargs) -> None: 46 | """Initialize the process with the associated parameters""" 47 | super().__init__(num_params=1, num_objectives=1, **kwargs) 48 | 49 | 50 | class DualInputFunction(BaseObjectiveFunction): 51 | """ 52 | An abstract process representing a dual input, single output 53 | test function. 54 | """ 55 | 56 | def __init__(self, **kwargs) -> None: 57 | """Initialize the process with the associated parameters""" 58 | super().__init__(num_params=2, num_objectives=1, **kwargs) 59 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Issue Number: 8 | 9 | 10 | Objective of pull request: 11 | 12 | ## Pull request checklist 13 | 14 | Your PR fulfills the following requirements: 15 | - [ ] [Issue](https://github.com/lava-nc/lava-optimization/issues) created that explains the change and why it's needed 16 | - [ ] Tests are part of the PR (for bug fixes / features) 17 | - [ ] [Docs](https://github.com/lava-nc/docs) reviewed and added / updated if needed (for bug fixes / features) 18 | - [ ] PR conforms to [Coding Conventions](https://lava-nc.org/developer_guide.html#coding-conventions) 19 | - [ ] [PR applys BSD 3-clause or LGPL2.1+ Licenses](https://lava-nc.org/developer_guide.html#add-a-license) to all code files 20 | - [ ] Lint (`pyb`) passes locally 21 | - [ ] Build tests (`pyb -E unit`) or (`python -m unittest`) passes locally 22 | 23 | 24 | ## Pull request type 25 | 26 | 27 | 28 | 29 | 30 | Please check your PR type: 31 | - [ ] Bugfix 32 | - [ ] Feature 33 | - [ ] Code style update (formatting, renaming) 34 | - [ ] Refactoring (no functional changes, no api changes) 35 | - [ ] Build related changes 36 | - [ ] Documentation changes 37 | - [ ] Other (please describe): 38 | 39 | 40 | ## What is the current behavior? 41 | 42 | - 43 | 44 | ## What is the new behavior? 45 | 46 | - 47 | 48 | ## Does this introduce a breaking change? 49 | 50 | - [ ] Yes 51 | - [ ] No 52 | 53 | 54 | 55 | 56 | ## Supplemental information 57 | 58 | 59 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/solution_reader/test_solution_reader.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import unittest 5 | 6 | import numpy as np 7 | from lava.lib.optimization.solvers.generic.solution_reader.process import ( 8 | SolutionReader, 9 | ) 10 | from lava.lib.optimization.solvers.generic.read_gate.models import \ 11 | get_read_gate_model_class 12 | from lava.lib.optimization.solvers.generic.read_gate.process import ReadGate 13 | from lava.magma.core.run_conditions import RunContinuous 14 | from lava.magma.core.run_configs import Loihi2SimCfg 15 | from lava.proc.spiker.models import SpikerModel 16 | from lava.proc.spiker.process import Spiker 17 | 18 | 19 | class TestSolutionReader(unittest.TestCase): 20 | def setUp(self) -> None: 21 | # Create processes. 22 | spiker = Spiker(shape=(4,), period=3, payload=7) 23 | # Together, these processes spike a cost of (-1<<24)+16777212 = -4 24 | integrator_last_bytes = Spiker(shape=(1,), period=7, payload=16777212) 25 | integrator_first_byte = Spiker(shape=(1,), period=7, payload=-1) 26 | 27 | self.solution_reader = SolutionReader( 28 | var_shape=(4,), target_cost=0, min_cost=2 29 | ) 30 | 31 | # Connect processes. 32 | integrator_last_bytes.s_out.connect( 33 | self.solution_reader.read_gate_in_port_last_bytes_0) 34 | integrator_first_byte.s_out.connect( 35 | self.solution_reader.read_gate_in_port_first_byte_0) 36 | self.solution_reader.ref_port.connect_var(spiker.payload) 37 | 38 | # Execution configurations. 39 | ReadGatePyModel = get_read_gate_model_class(1) 40 | pdict = {ReadGate: ReadGatePyModel, Spiker: SpikerModel} 41 | self.run_cfg = Loihi2SimCfg(exception_proc_model_map=pdict) 42 | self.solution_reader._log_config.level = 20 43 | 44 | def test_create_process(self): 45 | self.assertIsInstance(self.solution_reader, SolutionReader) 46 | 47 | def test_stops_when_desired_cost_reached(self): 48 | self.solution_reader.run(RunContinuous(), run_cfg=self.run_cfg) 49 | self.solution_reader.wait() 50 | solution = self.solution_reader.solution.get() 51 | self.solution_reader.stop() 52 | self.assertTrue(np.all(solution == np.zeros(4))) 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | Objective of issue: 15 | 16 | 17 | 18 | **Lava version:** 19 | - [ ] **0.3.0** (feature release) 20 | - [ ] **0.2.1** (bug fixes) 21 | - [x] **0.2.0** (current version) 22 | - [ ] **0.1.2** 23 | 24 | **I'm submitting a ...** 25 | 26 | - [ ] bug report 27 | - [ ] feature request 28 | - [ ] documentation request 29 | 30 | 31 | 32 | **Current behavior:** 33 | 34 | - 35 | 36 | **Expected behavior:** 37 | 38 | - 39 | 40 | **Steps to reproduce:** 41 | 42 | - 43 | 44 | **Related code:** 45 | 46 | 57 | 58 | ``` 59 | insert short code snippets here 60 | ``` 61 | 62 | **Other information:** 63 | 64 | 65 | 66 | ``` 67 | insert the output from lava debug here 68 | ``` 69 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/monitoring_processes/solution_readout/test_solution_readout.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import unittest 6 | 7 | import numpy as np 8 | 9 | from lava.lib.optimization.solvers.generic.monitoring_processes \ 10 | .solution_readout.process import \ 11 | SolutionReadout 12 | from lava.lib.optimization.solvers.generic.read_gate.models import \ 13 | get_read_gate_model_class 14 | from lava.lib.optimization.solvers.generic.read_gate.process import ReadGate 15 | from lava.magma.core.run_conditions import RunContinuous 16 | from lava.magma.core.run_configs import Loihi2SimCfg 17 | from lava.proc.spiker.models import SpikerModel 18 | from lava.proc.spiker.process import Spiker 19 | 20 | 21 | class TestSolutionReadout(unittest.TestCase): 22 | def setUp(self) -> None: 23 | # Create processes. 24 | spiker = Spiker(shape=(4,), period=3, payload=7) 25 | # Together, these processes spike a cost of (-1<<24)+16777212 = -4 26 | integrator_last_bytes = Spiker(shape=(1,), period=7, payload=16777212) 27 | integrator_first_byte = Spiker(shape=(1,), period=7, payload=-1) 28 | readgate = ReadGate(shape=(4,), target_cost=-3) 29 | self.readout = SolutionReadout(shape=(4,), target_cost=-3) 30 | 31 | # Connect processes. 32 | integrator_last_bytes.s_out.connect(readgate.cost_in_last_bytes_0) 33 | integrator_first_byte.s_out.connect(readgate.cost_in_first_byte_0) 34 | readgate.solution_reader.connect_var(spiker.payload) 35 | readgate.solution_out.connect(self.readout.read_solution) 36 | readgate.cost_out.connect(self.readout.cost_in) 37 | readgate.send_pause_request.connect(self.readout.timestep_in) 38 | 39 | # Execution configurations. 40 | ReadGatePyModel = get_read_gate_model_class(1) 41 | pdict = {ReadGate: ReadGatePyModel, Spiker: SpikerModel} 42 | self.run_cfg = Loihi2SimCfg(exception_proc_model_map=pdict) 43 | self.readout._log_config.level = 20 44 | 45 | def test_stops_when_desired_cost_reached(self): 46 | self.readout.run(RunContinuous(), run_cfg=self.run_cfg) 47 | self.readout.wait() 48 | solution = self.readout.solution.get() 49 | self.readout.stop() 50 | self.assertTrue(np.all(solution == np.zeros(4))) 51 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/tsp/test_solver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pprint 3 | import unittest 4 | 5 | import numpy as np 6 | import networkx as ntx 7 | from lava.lib.optimization.apps.tsp.problems import TravellingSalesmanProblem 8 | from lava.lib.optimization.apps.tsp.solver import TSPConfig, TSPSolver 9 | 10 | 11 | def get_bool_env_setting(env_var: str): 12 | """Get an environment varible and return 13 | True if the variable is set to 1 else return 14 | false 15 | """ 16 | env_test_setting = os.environ.get(env_var) 17 | test_setting = False 18 | if env_test_setting == "1": 19 | test_setting = True 20 | return test_setting 21 | 22 | 23 | run_loihi_tests: bool = get_bool_env_setting("RUN_LOIHI_TESTS") 24 | run_lib_tests: bool = get_bool_env_setting("RUN_LIB_TESTS") 25 | skip_reason = "Either Loihi or Lib or both tests are disabled." 26 | 27 | 28 | class TestTSPSolver(unittest.TestCase): 29 | def setUp(self) -> None: 30 | all_coords = [(1, 1), (2, 1), (16, 1), (16, 15), (2, 15)] 31 | self.center_coords = all_coords[0] 32 | self.point_coords = all_coords[1:] 33 | self.tsp_instance = TravellingSalesmanProblem( 34 | waypt_coords=self.point_coords, starting_pt=self.center_coords) 35 | 36 | def test_init(self): 37 | solver = TSPSolver(tsp=self.tsp_instance) 38 | self.assertIsInstance(solver, TSPSolver) 39 | 40 | @unittest.skipUnless(run_loihi_tests and run_lib_tests, skip_reason) 41 | def test_solve_lava_qubo(self): 42 | solver = TSPSolver(tsp=self.tsp_instance) 43 | scfg = TSPConfig(backend="Loihi2", 44 | hyperparameters={}, 45 | target_cost=-1000000, 46 | timeout=1000, 47 | probe_time=False, 48 | log_level=40) 49 | np.random.seed(0) 50 | solver.solve(scfg=scfg) 51 | gt_indices = np.array([2, 3, 4, 5]) 52 | spidx = solver.solution.solution_path_ids 53 | self.assertEqual(gt_indices.size, len(spidx)) 54 | 55 | gt_graph = ntx.Graph([(2, 3), (3, 4), (4, 5), (5, 2)]) 56 | sol_graph = ntx.Graph([(spidx[0], spidx[1]), 57 | (spidx[1], spidx[2]), 58 | (spidx[2], spidx[3]), 59 | (spidx[3], spidx[0])]) 60 | 61 | self.assertTrue(ntx.utils.graphs_equal(gt_graph, sol_graph)) 62 | 63 | 64 | if __name__ == '__main__': 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # VSCode settings 117 | .vscode/* 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # Flakeheaven Cache 134 | .flakeheaven_cache/* 135 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/solution_reader/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | from lava.lib.optimization.solvers.generic.read_gate.process import ReadGate 5 | from lava.lib.optimization.solvers.generic.solution_reader.process import ( 6 | SolutionReader, 7 | ) 8 | from lava.lib.optimization.solvers.generic.monitoring_processes\ 9 | .solution_readout.process import SolutionReadout 10 | from lava.magma.core.decorator import implements 11 | from lava.magma.core.model.sub.model import AbstractSubProcessModel 12 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 13 | 14 | 15 | @implements(proc=SolutionReader, protocol=LoihiProtocol) 16 | class SolutionReaderModel(AbstractSubProcessModel): 17 | def __init__(self, proc): 18 | var_shape = proc.proc_params.get("var_shape") 19 | target_cost = proc.proc_params.get("target_cost") 20 | num_in_ports = proc.proc_params.get("num_in_ports") 21 | time_steps_per_algorithmic_step = proc.proc_params.get( 22 | "time_steps_per_algorithmic_step") 23 | 24 | self.read_gate = ReadGate( 25 | shape=var_shape, target_cost=target_cost, num_in_ports=num_in_ports 26 | ) 27 | self.solution_readout = SolutionReadout( 28 | shape=var_shape, 29 | target_cost=target_cost, 30 | time_steps_per_algorithmic_step=time_steps_per_algorithmic_step, 31 | ) 32 | self.read_gate.cost_out.connect(self.solution_readout.cost_in) 33 | self.read_gate.solution_out.connect(self.solution_readout.read_solution) 34 | self.read_gate.send_pause_request.connect( 35 | self.solution_readout.timestep_in 36 | ) 37 | 38 | proc.vars.solution.alias(self.solution_readout.solution) 39 | proc.vars.min_cost.alias(self.solution_readout.min_cost) 40 | proc.vars.solution_step.alias(self.solution_readout.solution_step) 41 | 42 | self.read_gate.solution_reader.connect(proc.ref_ports.ref_port) 43 | for id in range(num_in_ports): 44 | in_port = getattr(proc.in_ports, 45 | f"read_gate_in_port_first_byte_{id}") 46 | out_port = getattr(self.read_gate, f"cost_in_first_byte_{id}") 47 | in_port.connect(out_port) 48 | in_port = getattr(proc.in_ports, 49 | f"read_gate_in_port_last_bytes_{id}") 50 | out_port = getattr(self.read_gate, f"cost_in_last_bytes_{id}") 51 | in_port.connect(out_port) 52 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/problems/coefficients.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import typing as ty 6 | 7 | import numpy as np 8 | import numpy.typing as npt 9 | 10 | 11 | class CoefficientTensorsMixin: 12 | def __init__(self, *coefficients: ty.Union[ty.List, npt.ArrayLike]): 13 | """Coefficients for a scalar function of a vector. 14 | 15 | Parameters 16 | ---------- 17 | coefficients: the tensor coefficients of the function. 18 | """ 19 | c_dict = dict() 20 | for coefficient in coefficients: 21 | if coefficient is None: 22 | continue 23 | if type(coefficient) in [list, int]: 24 | coefficient = np.asarray(coefficient) 25 | rank = coefficient.ndim 26 | elif type(coefficient) is not np.ndarray: 27 | raise ValueError( 28 | "Coefficients should be either Numpy arrays " 29 | "or (possibly nested) lists." 30 | ) 31 | else: 32 | rank = np.squeeze(coefficient).ndim 33 | c_dict[rank] = coefficient 34 | self._coefficients = c_dict 35 | 36 | @property 37 | def coefficients(self): 38 | return self._coefficients 39 | 40 | @coefficients.setter 41 | def coefficients(self, value): 42 | self._coefficients = value 43 | 44 | def get_coefficient(self, order: int): 45 | try: 46 | return self.coefficients[order] 47 | except KeyError: 48 | print( 49 | f"""Order {order} not found, coefficients were only given for 50 | orders: {list(self.coefficients.keys())}.""" 51 | ) 52 | raise 53 | 54 | @property 55 | def max_degree(self): 56 | """Maximum order among the coefficients' ranks.""" 57 | return max(self.coefficients.keys()) 58 | 59 | def evaluate(self, x: np.ndarray) -> np.ndarray: 60 | """Evaluate the polynomial at the given point.""" 61 | result = 0 62 | for rank, coeff in self._coefficients.items(): 63 | if rank == 0: 64 | result += coeff 65 | elif rank == 1: 66 | result += coeff @ x 67 | elif rank == 2: 68 | result += x.T @ coeff @ x 69 | else: 70 | raise NotImplementedError( 71 | "__call__ not implemented for polynomials with degree > 2." 72 | ) 73 | return result 74 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/monitoring_processes/solution_readout/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import numpy as np 5 | import typing as ty 6 | 7 | from lava.magma.core.process.ports.ports import InPort 8 | from lava.magma.core.process.process import AbstractProcess, LogConfig 9 | from lava.magma.core.process.variable import Var 10 | 11 | 12 | class SolutionReadout(AbstractProcess): 13 | """Process to readout solution from SNN and make it available on host. 14 | 15 | Parameters 16 | ---------- 17 | shape: The shape of the set of nodes, or process, which state will be read. 18 | target_cost: cost value at which, once attained by the network, 19 | this process will stop execution. 20 | name: Name of the Process. Default is 'Process_ID', where ID is an 21 | integer value that is determined automatically. 22 | log_config: Configuration options for logging. 23 | time_steps_per_algorithmic_step: the number of iteration steps that a 24 | single algorithmic step requires. This value is required to decode the 25 | variable values from the spk_hist of a process. 26 | 27 | Attributes 28 | ---------- 29 | read_solution: InPort 30 | A message received on this ports signifies the process 31 | should call read on its RefPort. 32 | ref_port: RefPort 33 | A reference port to a variable in another process which state 34 | will be remotely accessed upon read request. Here, it reads the 35 | current variables assignment by a solver to an optimization problem. 36 | target_cost: Var 37 | Cost value at which, once attained by the network. 38 | 39 | """ 40 | 41 | def __init__( 42 | self, 43 | shape: ty.Tuple[int, ...], 44 | target_cost=None, 45 | time_steps_per_algorithmic_step: int = 1, 46 | name: ty.Optional[str] = None, 47 | log_config: ty.Optional[LogConfig] = None, 48 | ) -> None: 49 | super().__init__( 50 | shape=shape, 51 | target_cost=target_cost, 52 | name=name, 53 | log_config=log_config, 54 | ) 55 | self.solution = Var(shape=shape, init=-1) 56 | self.solution_step = Var(shape=(1,), init=-1) 57 | self.min_cost = Var(shape=(2,), init=-1) 58 | self.target_cost = Var(shape=(1,), init=target_cost) 59 | self.time_steps_per_algorithmic_step = Var( 60 | shape=(1,), 61 | init=time_steps_per_algorithmic_step) 62 | self.read_solution = InPort(shape=shape) 63 | self.cost_in = InPort(shape=(2,)) 64 | self.timestep_in = InPort(shape=(1,)) 65 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/problems/bayesian/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: LGPL-2.1-or-later 3 | # See: https://spdx.org/licenses/ 4 | 5 | import math 6 | import numpy as np 7 | 8 | from lava.magma.core.decorator import implements, requires, tag 9 | from lava.magma.core.model.py.model import PyLoihiProcessModel 10 | from lava.magma.core.model.py.ports import PyInPort, PyOutPort 11 | from lava.magma.core.model.py.type import LavaPyType 12 | from lava.magma.core.resources import CPU 13 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 14 | 15 | from lava.lib.optimization.problems.bayesian.processes import ( 16 | DualInputFunction, 17 | SingleInputFunction 18 | ) 19 | 20 | 21 | @implements(proc=SingleInputFunction, protocol=LoihiProtocol) 22 | @requires(CPU) 23 | @tag('floating_pt') 24 | class PySingleInputFunctionModel(PyLoihiProcessModel): 25 | """ 26 | A Python-based implementation of the SingleInput process that represents a 27 | single input/output non-linear objective function. 28 | """ 29 | 30 | x_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.float64) 31 | y_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, np.float64) 32 | 33 | num_params = LavaPyType(int, int) 34 | num_objectives = LavaPyType(int, int) 35 | 36 | def run_spk(self) -> None: 37 | """tick the model forward by one time-step""" 38 | x = self.x_in.recv() 39 | y = math.cos(x) * math.sin(x) + (x * x / 25) 40 | 41 | output_length: int = self.num_params + self.num_objectives 42 | output = np.ndarray( 43 | shape=(output_length, 1), 44 | buffer=np.array([x, y]) 45 | ) 46 | 47 | self.y_out.send(output) 48 | 49 | 50 | @implements(proc=DualInputFunction, protocol=LoihiProtocol) 51 | @requires(CPU) 52 | @tag('floating_pt') 53 | class PyDualInputFunctionModel(PyLoihiProcessModel): 54 | """ 55 | A Python-based implementation of the DualInputFunction process that 56 | represents a dual continuous input, single output, non-linear objective 57 | function. 58 | """ 59 | 60 | x_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.float64) 61 | y_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, np.float64) 62 | 63 | num_params = LavaPyType(int, int) 64 | num_objectives = LavaPyType(int, int) 65 | 66 | def run_spk(self) -> None: 67 | """tick the model forward by one time-step""" 68 | 69 | x = self.x_in.recv() 70 | y = math.sin(x[1] * x[0]) + (0.2 * x[0]) ** 2 + math.cos(x[1]) 71 | 72 | output_length: int = self.num_objectives + self.num_params 73 | output = np.ndarray( 74 | shape=(output_length, 1), 75 | buffer=np.array([x[0], x[1], y]) 76 | ) 77 | 78 | self.y_out.send(output) 79 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/cost_integrator/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | from lava.magma.core.decorator import implements, requires 7 | from lava.magma.core.model.py.model import PyLoihiProcessModel 8 | from lava.magma.core.model.py.ports import PyInPort, PyOutPort 9 | from lava.magma.core.model.py.type import LavaPyType 10 | from lava.magma.core.resources import CPU 11 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 12 | 13 | from lava.lib.optimization.solvers.generic.cost_integrator.process import ( 14 | CostIntegrator, 15 | ) 16 | 17 | 18 | @implements(proc=CostIntegrator, protocol=LoihiProtocol) 19 | @requires(CPU) 20 | class CostIntegratorModel(PyLoihiProcessModel): 21 | """CPU model for the CostIntegrator process. 22 | 23 | The process adds up local cost components from downstream units comming as 24 | spike payload. It has a cost_min variable which keeps track of the best 25 | cost seen so far, if the new cost is better, the minimum cost is updated 26 | and send as an output spike to an upstream process. 27 | Note that cost_min is divided into the first and last three bytes. 28 | cost_min = cost_min_first_byte << 24 + cost_min_last_bytes 29 | """ 30 | 31 | cost_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, int) 32 | cost_out_last_bytes: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, int) 33 | cost_out_first_byte: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, int) 34 | cost_min_last_bytes: np.ndarray = LavaPyType(np.ndarray, int, 24) 35 | cost_min_first_byte: np.ndarray = LavaPyType(np.ndarray, int, 8) 36 | cost_last_bytes: np.ndarray = LavaPyType(np.ndarray, int, 24) 37 | cost_first_byte: np.ndarray = LavaPyType(np.ndarray, int, 8) 38 | 39 | def run_spk(self): 40 | """Execute spiking phase, integrate input, update dynamics and send 41 | messages out.""" 42 | cost = self.cost_in.recv() 43 | # Clip cost to 32bit 44 | np.clip(cost, -2 ** 31, 2 ** 31 - 1, out=cost) 45 | # Distribute into components 46 | cost = cost.astype(int) 47 | cost_last_bytes = cost & (2**24 - 1) # maintain last 24 bit 48 | cost_first_byte = (cost & ((2**8 - 1) << 24)) >> 24 # first 8 bit 49 | cost_first_byte = np.array(cost_first_byte).astype(np.int8) # sint8 50 | if cost < (self.cost_min_first_byte << 24) + self.cost_min_last_bytes: 51 | self.cost_min_first_byte[:] = cost_first_byte 52 | self.cost_min_last_bytes[:] = cost_last_bytes 53 | self.cost_out_last_bytes.send(cost_last_bytes) 54 | self.cost_out_first_byte.send(cost_first_byte) 55 | else: 56 | self.cost_out_last_bytes.send(np.asarray([0])) 57 | self.cost_out_first_byte.send(np.asarray([0])) 58 | self.cost_last_bytes[:] = cost_last_bytes 59 | self.cost_first_byte[:] = cost_first_byte 60 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/lca/v1_neuron/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Battelle Memorial Institute 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | from lava.magma.core.decorator import implements, requires, tag 7 | from lava.magma.core.model.py.model import PyLoihiProcessModel 8 | from lava.magma.core.model.py.ports import PyInPort, PyOutPort 9 | from lava.magma.core.model.py.type import LavaPyType 10 | from lava.magma.core.resources import CPU 11 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 12 | 13 | from lava.lib.optimization.solvers.lca.v1_neuron.process import V1Neuron 14 | from lava.lib.optimization.solvers.lca.util import apply_activation 15 | 16 | 17 | @implements(proc=V1Neuron, protocol=LoihiProtocol) 18 | @requires(CPU) 19 | @tag('floating_pt') 20 | class PyV1NeuronFloat(PyLoihiProcessModel): 21 | # This model might spike too frequently. Implement an accumulator if so. 22 | vth: float = LavaPyType(float, float) 23 | tau: float = LavaPyType(float, float) 24 | tau_exp: int = LavaPyType(int, int) 25 | a_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, float) 26 | s_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, float) 27 | v: np.ndarray = LavaPyType(np.ndarray, float) 28 | bias: np.ndarray = LavaPyType(np.ndarray, float) 29 | 30 | def __init__(self, proc_params): 31 | super(PyV1NeuronFloat, self).__init__(proc_params) 32 | self.tau_float = np.ldexp(proc_params["tau"], 33 | proc_params["tau_exp"]) 34 | 35 | def run_spk(self): 36 | # Soft-threshold activation 37 | activation = apply_activation(self.v, self.vth) 38 | bias = activation * self.tau_float if self.proc_params['two_layer'] \ 39 | else self.bias 40 | self.v = self.v * (1 - self.tau_float) + self.a_in.recv() + bias 41 | self.s_out.send(activation) 42 | 43 | 44 | @implements(proc=V1Neuron, protocol=LoihiProtocol) 45 | @requires(CPU) 46 | @tag('fixed_pt') 47 | class PyV1NeuronFixed(PyLoihiProcessModel): 48 | vth: int = LavaPyType(int, int) 49 | tau: int = LavaPyType(int, int) 50 | tau_exp: int = LavaPyType(int, int) 51 | a_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, int) 52 | s_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, int) 53 | v: np.ndarray = LavaPyType(np.ndarray, int) 54 | bias: np.ndarray = LavaPyType(np.ndarray, int) 55 | 56 | def __init__(self, proc_params): 57 | super(PyV1NeuronFixed, self).__init__(proc_params) 58 | self.tau_int = int(np.ldexp(proc_params["tau"], 59 | proc_params["tau_exp"] + 24)) 60 | 61 | def run_spk(self): 62 | # Soft-threshold activation 63 | activation = apply_activation(self.v, self.vth) 64 | bias = np.right_shift(activation * self.tau_int, 24) \ 65 | if self.proc_params['two_layer'] else self.bias 66 | self.v = np.right_shift( 67 | self.v * (2 ** 24 - self.tau_int), 24) + self.a_in.recv() + bias 68 | self.s_out.send(activation) 69 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/tsp/utils/test_q_matrix_generator.py: -------------------------------------------------------------------------------- 1 | # INTEL CORPORATION CONFIDENTIAL AND PROPRIETARY 2 | # 3 | # Copyright © 2023 Intel Corporation. 4 | # 5 | # This software and the related documents are Intel copyrighted 6 | # materials, and your use of them is governed by the express 7 | # license under which they were provided to you (License). Unless 8 | # the License provides otherwise, you may not use, modify, copy, 9 | # publish, distribute, disclose or transmit this software or the 10 | # related documents without Intel's prior written permission. 11 | # 12 | # This software and the related documents are provided as is, with 13 | # no express or implied warranties, other than those that are 14 | # expressly stated in the License. 15 | import unittest 16 | import numpy as np 17 | from lava.lib.optimization.apps.tsp.utils.q_matrix_generator import \ 18 | QMatrixTSP 19 | 20 | 21 | class TestMatrixGen(unittest.TestCase): 22 | def test_Q_gen(self) -> None: 23 | input_nodes = [(0, 1), (0, 0), (0, -1)] 24 | lamda_dist = 2.5 25 | lamda_cnstrt = 4 # testing floating point Q matrix 26 | q_mat_obj_flp = QMatrixTSP( 27 | input_nodes, 28 | lamda_dist=lamda_dist, 29 | lamda_cnstrt=lamda_cnstrt, 30 | ) 31 | q_mat_flp = q_mat_obj_flp.matrix 32 | q_dist_scaled_test = np.array( 33 | [ 34 | [0, 0, 0, 0, 1, 2, 0, 1, 2], 35 | [0, 0, 0, 1, 0, 1, 1, 0, 1], 36 | [0, 0, 0, 2, 1, 0, 2, 1, 0], 37 | [0, 1, 2, 0, 0, 0, 0, 1, 2], 38 | [1, 0, 1, 0, 0, 0, 1, 0, 1], 39 | [2, 1, 0, 0, 0, 0, 2, 1, 0], 40 | [0, 1, 2, 0, 1, 2, 0, 0, 0], 41 | [1, 0, 1, 1, 0, 1, 0, 0, 0], 42 | [2, 1, 0, 2, 1, 0, 0, 0, 0], 43 | ] 44 | ) 45 | q_cnstrnts_test = 2 * np.array( 46 | [ 47 | [-1, 1, 1, 1, 0, 0, 1, 0, 0], 48 | [1, -1, 1, 0, 1, 0, 0, 1, 0], 49 | [1, 1, -1, 0, 0, 1, 0, 0, 1], 50 | [1, 0, 0, -1, 1, 1, 1, 0, 0], 51 | [0, 1, 0, 1, -1, 1, 0, 1, 0], 52 | [0, 0, 1, 1, 1, -1, 0, 0, 1], 53 | [1, 0, 0, 1, 0, 0, -1, 1, 1], 54 | [0, 1, 0, 0, 1, 0, 1, -1, 1], 55 | [0, 0, 1, 0, 0, 1, 1, 1, -1], 56 | ] 57 | ) 58 | 59 | q_test = ( 60 | lamda_dist * q_dist_scaled_test + lamda_cnstrt * q_cnstrnts_test 61 | ) 62 | self.assertTrue(np.all(q_mat_flp == q_test)) 63 | # testing fixed point Q matrix 64 | # individual values not really testsed since the rounding 65 | # is stochastic 66 | q_mat_obj_fxp = QMatrixTSP(input_nodes, 67 | fixed_pt=True) 68 | q_mat_fxp = q_mat_obj_fxp.matrix 69 | q_max_fxp = np.max(np.abs(q_mat_fxp)) 70 | q_min_fxp = np.min(np.abs(q_mat_fxp)) 71 | self.assertGreaterEqual(q_min_fxp, 0, True) 72 | self.assertLessEqual(q_max_fxp, 127, True) 73 | 74 | 75 | if __name__ == "__main__": 76 | unittest.main() 77 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/clustering/test_solver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import numpy as np 5 | 6 | from lava.lib.optimization.apps.clustering.problems import ClusteringProblem 7 | from lava.lib.optimization.apps.clustering.solver import (ClusteringConfig, 8 | ClusteringSolver) 9 | 10 | 11 | def get_bool_env_setting(env_var: str): 12 | """Get an environment varible and return 13 | True if the variable is set to 1 else return 14 | false 15 | """ 16 | env_test_setting = os.environ.get(env_var) 17 | test_setting = False 18 | if env_test_setting == "1": 19 | test_setting = True 20 | return test_setting 21 | 22 | 23 | run_loihi_tests: bool = get_bool_env_setting("RUN_LOIHI_TESTS") 24 | run_lib_tests: bool = get_bool_env_setting("RUN_LIB_TESTS") 25 | skip_reason = "Either Loihi or Lib or both tests are disabled." 26 | 27 | 28 | class TestClusteringSolver(unittest.TestCase): 29 | def setUp(self) -> None: 30 | all_coords = [(1, 1), (23, 23), (2, 1), (3, 2), (1, 3), 31 | (4, 2), (22, 21), (20, 23), (21, 21), (24, 25)] 32 | self.center_coords = all_coords[:2] 33 | self.point_coords = all_coords[2:] 34 | self.edges = [(1, 3), (2, 9), (3, 5), (3, 4), (4, 6), (6, 5), 35 | (9, 7), (8, 10), (7, 10), (10, 8), (7, 9)] 36 | self.clustering_prob_instance = ClusteringProblem( 37 | point_coords=self.point_coords, center_coords=self.center_coords) 38 | 39 | def test_init(self): 40 | solver = ClusteringSolver(clp=self.clustering_prob_instance) 41 | self.assertIsInstance(solver, ClusteringSolver) 42 | 43 | @unittest.skipUnless(run_loihi_tests and run_lib_tests, skip_reason) 44 | def test_solve_lava_qubo(self): 45 | solver = ClusteringSolver(clp=self.clustering_prob_instance) 46 | scfg = ClusteringConfig(backend="Loihi2", 47 | hyperparameters={}, 48 | target_cost=-1000000, 49 | timeout=1000, 50 | probe_time=False, 51 | log_level=40) 52 | np.random.seed(0) 53 | solver.solve(scfg=scfg) 54 | gt_id_map = {1: [3, 4, 5, 6], 2: [7, 8, 9, 10]} 55 | self.assertSetEqual(set(gt_id_map.keys()), 56 | set(solver.solution.clustering_id_map.keys())) 57 | for cluster_id, cluster in gt_id_map.items(): 58 | self.assertSetEqual( 59 | set(cluster), 60 | set(solver.solution.clustering_id_map[cluster_id])) 61 | gt_coord_map = {self.center_coords[0]: self.point_coords[:4], 62 | self.center_coords[1]: self.point_coords[4:]} 63 | self.assertSetEqual(set(gt_coord_map.keys()), 64 | set(solver.solution.clustering_coords_map.keys())) 65 | for center_coords, points in gt_coord_map.items(): 66 | self.assertSetEqual( 67 | set(points), 68 | set(solver.solution.clustering_coords_map[center_coords])) 69 | 70 | 71 | if __name__ == '__main__': 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/problems/test_coefficients.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import unittest 6 | 7 | import numpy as np 8 | 9 | from lava.lib.optimization.problems.coefficients import CoefficientTensorsMixin 10 | 11 | 12 | class TestCoefficientsTensors(unittest.TestCase): 13 | def setUp(self) -> None: 14 | self.coefficients_np = CoefficientTensorsMixin( 15 | np.asarray(1), np.ones(2), np.ones((2, 2)), np.ones((2, 2, 2)) 16 | ) 17 | self.coefficients_list = CoefficientTensorsMixin( 18 | 1, [1, 1], [[1, 1], [1, 1]], [[[1, 1], [1, 1]], [[1, 1], [1, 1]]] 19 | ) 20 | 21 | def test_create_obj(self): 22 | for coeffs, name in ( 23 | (self.coefficients_list, "Lists"), 24 | (self.coefficients_np, "Numpy"), 25 | ): 26 | with self.subTest(msg=f"Test for input as {name}"): 27 | self.assertIsInstance(coeffs, CoefficientTensorsMixin) 28 | 29 | def test_get_coefficient(self): 30 | coeff_rank0 = (np.ones(2),) 31 | coeff_rank1 = (np.ones((2, 2)),) 32 | coeff_rank2 = (np.ones((2, 2)),) 33 | coeff_rank3 = np.ones((2, 2, 2)) 34 | expected = (coeff_rank0, coeff_rank1, coeff_rank2, coeff_rank3) 35 | for order in range(4): 36 | with self.subTest(msg=f"Test for order {order}"): 37 | result = self.coefficients_np.get_coefficient(order=order) 38 | self.assertTrue((result == expected[order]).all()) 39 | 40 | def test_trying_to_get_absent_coefficient(self): 41 | with self.assertRaises(KeyError): 42 | self.coefficients_np.get_coefficient(order=5) 43 | 44 | def test_getting_the_max_degree(self): 45 | self.assertEqual(self.coefficients_np.max_degree, 3) 46 | 47 | def test_is_ok_not_providing_intermediate_term(self): 48 | coefficients = CoefficientTensorsMixin( 49 | np.asarray(1), np.ones((2, 2)), np.ones((2, 2, 2)) 50 | ) 51 | self.assertIsInstance(coefficients, CoefficientTensorsMixin) 52 | provided_ranks = sorted(list(coefficients.coefficients.keys())) 53 | self.assertEqual(provided_ranks, [0, 2, 3]) 54 | 55 | def test_wrong_input_raises_exception(self): 56 | for value in ["input_string", (1, 2, 3)]: 57 | with self.subTest(msg=f"test for {value} as wrong input"): 58 | with self.assertRaises(ValueError): 59 | CoefficientTensorsMixin(value) 60 | 61 | def test_set_coefficients(self): 62 | new_coefficients = ( 63 | np.asarray(5), 64 | 4 * np.ones(2), 65 | 3 * np.ones((2, 2)), 66 | 2 * np.ones((2, 2, 2)), 67 | ) 68 | self.coefficients_np.coefficients = new_coefficients 69 | self.assertIs(self.coefficients_np.coefficients, new_coefficients) 70 | 71 | def test_evaluate_method(self): 72 | ctm = CoefficientTensorsMixin( 73 | 1, [1, 1], [[1, 1], [1, 1]] 74 | ) 75 | self.assertEqual(ctm.evaluate(np.array([1, 1])), 7) 76 | 77 | 78 | if __name__ == "__main__": 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/schduler/test_problems.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import pprint 5 | import unittest 6 | 7 | import numpy as np 8 | 9 | from lava.lib.optimization.apps.scheduler.problems import ( 10 | SchedulingProblem, SatelliteScheduleProblem) 11 | 12 | 13 | class TestSchedulingProblem(unittest.TestCase): 14 | 15 | def setUp(self) -> None: 16 | self.sp = SchedulingProblem(num_agents=3, num_tasks=3) 17 | 18 | def test_init(self): 19 | self.assertIsInstance(self.sp, SchedulingProblem) 20 | 21 | def test_generate(self): 22 | self.sp.generate(seed=42) 23 | nodeids = list(self.sp.graph.nodes.keys()) 24 | nodedicts = list(self.sp.graph.nodes.values()) 25 | self.assertListEqual(list(range(9)), nodeids) 26 | for j in range(3): 27 | for k in range(3): 28 | self.assertTupleEqual((nodedicts[3 * j + k]['agent_id'], 29 | nodedicts[3 * j + k]['task_id']), 30 | (j, k)) 31 | 32 | 33 | class TestSatelliteSchedulingProblem(unittest.TestCase): 34 | 35 | def setUp(self) -> None: 36 | requests = np.array( 37 | [[0.02058449, 0.96990985], [0.05808361, 0.86617615], 38 | [0.15601864, 0.15599452], [0.18182497, 0.18340451], 39 | [0.29214465, 0.36636184], [0.30424224, 0.52475643], 40 | [0.37454012, 0.95071431], [0.43194502, 0.29122914], 41 | [0.60111501, 0.70807258], [0.61185289, 0.13949386], 42 | [0.73199394, 0.59865848], [0.83244264, 0.21233911]] 43 | ) 44 | self.ssp = SatelliteScheduleProblem(num_satellites=3, 45 | num_requests=12, 46 | requests=requests, 47 | view_height=0.5, 48 | seed=42) 49 | 50 | def test_init(self): 51 | self.assertIsInstance(self.ssp, SatelliteScheduleProblem) 52 | 53 | def test_generate(self): 54 | self.ssp.generate(seed=42) 55 | gt_graph_dict = {0: {'agent_attr': (0.25, 0.5), 56 | 'agent_id': 1, 57 | 'task_attr': [0.30424224, 0.52475643], 58 | 'task_id': 5}, 59 | 1: {'agent_attr': (0.25, 0.5), 60 | 'agent_id': 1, 61 | 'task_attr': [0.73199394, 0.59865848], 62 | 'task_id': 10}, 63 | 2: {'agent_attr': (0.25, 1.0), 64 | 'agent_id': 2, 65 | 'task_attr': [0.02058449, 0.96990985], 66 | 'task_id': 0}, 67 | 3: {'agent_attr': (0.25, 1.0), 68 | 'agent_id': 2, 69 | 'task_attr': [0.37454012, 0.95071431], 70 | 'task_id': 6}} 71 | self.assertDictEqual(gt_graph_dict, dict(self.ssp.graph.nodes)) 72 | 73 | 74 | if __name__ == '__main__': 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/read_gate/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import typing as ty 5 | 6 | from lava.magma.core.process.ports.ports import InPort, OutPort, RefPort 7 | from lava.magma.core.process.process import AbstractProcess, LogConfig 8 | from lava.magma.core.process.variable import Var 9 | 10 | 11 | class ReadGate(AbstractProcess): 12 | """Process that triggers solution readout when problem is solved. 13 | 14 | Parameters 15 | ---------- 16 | shape: The shape of the set of units in the downstream process whose state 17 | will be read by ReadGate. 18 | target_cost: cost value at which, once attained by the network, 19 | this process will stop execution. 20 | name: Name of the Process. Default is 'Process_ID', where ID is an 21 | integer value that is determined automatically. 22 | log_config: Configuration options for logging. 23 | 24 | InPorts 25 | ------- 26 | cost_in_last_bytes: OutPort 27 | Receives a better cost found by the CostIntegrator at the 28 | previous timestep. 29 | Messages the last 3 byte of the new best cost. 30 | Total cost = cost_in_first_byte << 24 + cost_in_last_bytes. 31 | cost_in_first_byte: OutPort 32 | Receives a better cost found by the CostIntegrator at the 33 | previous timestep. 34 | Messages the first byte of the new best cost. 35 | 36 | OutPorts 37 | -------- 38 | cost_out: Forwards to an upstream process the better cost notified by the 39 | CostIntegrator. 40 | solution_out: Forwards to an upstream process the better variable assignment 41 | found by the solver network. 42 | send_pause_request: Notifies upstream process to request execution to pause. 43 | """ 44 | 45 | def __init__( 46 | self, 47 | shape: ty.Tuple[int, ...], 48 | target_cost=None, 49 | num_in_ports=1, 50 | name: ty.Optional[str] = None, 51 | log_config: ty.Optional[LogConfig] = None, 52 | ) -> None: 53 | super().__init__( 54 | shape=shape, 55 | target_cost=target_cost, 56 | num_in_ports=num_in_ports, 57 | name=name, 58 | log_config=log_config, 59 | ) 60 | self.target_cost = Var(shape=(1,), init=target_cost) 61 | 62 | self.best_solution = Var(shape=shape, init=-1) 63 | for id in range(num_in_ports): 64 | # Cost is transferred as two separate values 65 | # cost_last_bytes = last 3 byte of cost 66 | # cost_first_byte = first byte of cost 67 | # total cost = np.int8(cost_first_byte) << 24 + cost_last_bytes 68 | setattr(self, f"cost_in_last_bytes_{id}", InPort(shape=(1,))) 69 | setattr(self, f"cost_in_first_byte_{id}", InPort(shape=(1,))) 70 | self.cost_out = OutPort(shape=(2,)) 71 | self.best_solution = Var(shape=shape, init=-1) 72 | self.send_pause_request = OutPort(shape=(1,)) 73 | self.solution_out = OutPort(shape=shape) 74 | self.solution_reader = RefPort(shape=shape) 75 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/dataclasses.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | from dataclasses import dataclass 5 | 6 | from lava.lib.optimization.solvers.generic.hierarchical_processes import ( 7 | AugmentedTermsProcess, 8 | ContinuousConstraintsProcess, 9 | ContinuousVariablesProcess, 10 | DiscreteConstraintsProcess, 11 | DiscreteVariablesProcess, 12 | MixedConstraintsProcess, 13 | ) 14 | from lava.proc.dense.process import Dense 15 | from lava.proc.sparse.process import Sparse 16 | 17 | 18 | @dataclass 19 | class CostMinimizer: 20 | """Processes implementing an optimization problem's cost function.""" 21 | 22 | coefficients_2nd_order: Sparse 23 | 24 | @property 25 | def state_in(self): 26 | """Port receiving input from dynamical systems representing 27 | variables.""" 28 | return self.coefficients_2nd_order.s_in 29 | 30 | @property 31 | def gradient_out(self): 32 | """Port sending gradient descent components to the dynamical systems.""" 33 | return self.coefficients_2nd_order.a_out 34 | 35 | 36 | @dataclass 37 | class ConstraintEnforcing: 38 | """Processes implementing an optimization problem's constraints and their 39 | enforcing.""" 40 | 41 | continuous: ContinuousConstraintsProcess = None 42 | discrete: DiscreteConstraintsProcess = None 43 | mixed: MixedConstraintsProcess = None 44 | 45 | @property 46 | def state_in(self): 47 | return self.continuous.a_in 48 | 49 | @property 50 | def state_out(self): 51 | return self.continuous.s_out 52 | 53 | @property 54 | def variables_assignment(self): 55 | return self.continuous.constraint_assignment 56 | 57 | 58 | @dataclass() 59 | class VariablesImplementation: 60 | """Processes implementing the variables of an optimization problem.""" 61 | 62 | continuous: ContinuousVariablesProcess = None 63 | discrete: DiscreteVariablesProcess = None 64 | 65 | @property 66 | def gradient_in(self): 67 | return self.discrete.a_in 68 | 69 | @property 70 | def gradient_in_cont(self): 71 | return self.continuous.a_in 72 | 73 | @property 74 | def state_out(self): 75 | return self.discrete.s_out 76 | 77 | @property 78 | def state_out_cont(self): 79 | return self.continuous.s_out 80 | 81 | @property 82 | def importances(self): 83 | return self.discrete.cost_diagonal 84 | 85 | @importances.setter 86 | def importances(self, value): 87 | self.discrete.cost_diagonal = value 88 | 89 | @property 90 | def local_cost(self): 91 | return self.discrete.local_cost 92 | 93 | @property 94 | def variables_assignment(self): 95 | return self.discrete.variable_assignment 96 | 97 | @property 98 | def variables_assignment_cont(self): 99 | return self.continuous.variable_assignment 100 | 101 | 102 | @dataclass 103 | class ProximalGradientMinimizer: 104 | augmented_terms: AugmentedTermsProcess 105 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/cost_integrator/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import typing as ty 5 | 6 | import numpy as np 7 | from lava.magma.core.process.ports.ports import InPort, OutPort 8 | from lava.magma.core.process.process import AbstractProcess, LogConfig 9 | from lava.magma.core.process.variable import Var 10 | 11 | 12 | class CostIntegrator(AbstractProcess): 13 | """Node that integrates cost components and produces output when a better 14 | cost is found. 15 | 16 | Parameters 17 | ---------- 18 | shape : tuple(int) 19 | The expected number and topology of the input cost components. 20 | name : str, optional 21 | Name of the Process. Default is 'Process_ID', where ID is an 22 | integer value that is determined automatically. 23 | log_config: Configuration options for logging. 24 | 25 | InPorts 26 | ------- 27 | cost_in 28 | input to be additively integrated. 29 | 30 | OutPorts 31 | -------- 32 | cost_out_last_bytes: OutPort 33 | Notifies the next process about the detection of a better cost. 34 | Messages the last 3 byte of the new best cost. 35 | Total cost = cost_out_first_byte << 24 + cost_out_last_bytes. 36 | cost_out_first_byte: OutPort 37 | Notifies the next process about the detection of a better cost. 38 | Messages the first byte of the new best cost. 39 | 40 | Vars 41 | ---- 42 | cost 43 | Holds current cost as addition of input spikes' payloads 44 | 45 | cost_min_last_bytes 46 | Current minimum cost, i.e., the lowest reported cost so far. 47 | Saves the last 3 bytes. 48 | cost_min = cost_min_first_byte << 24 + cost_min_last_bytes 49 | cost_min_first_byte 50 | Current minimum cost, i.e., the lowest reported cost so far. 51 | Saves the first byte. 52 | """ 53 | 54 | def __init__( 55 | self, 56 | *, 57 | shape: ty.Tuple[int, ...] = (1,), 58 | min_cost: int = 0, # trivial solution, where all variables are 0 59 | name: ty.Optional[str] = None, 60 | log_config: ty.Optional[LogConfig] = None, 61 | ) -> None: 62 | super().__init__(shape=shape, name=name, log_config=log_config) 63 | self.cost_in = InPort(shape=shape) 64 | self.cost_out_last_bytes = OutPort(shape=shape) 65 | self.cost_out_first_byte = OutPort(shape=shape) 66 | 67 | # Current cost initiated to zero 68 | # Note: Total cost = cost_first_byte << 24 + cost_last_bytes 69 | self.cost_last_bytes = Var(shape=shape, init=0) 70 | # first 8 bit of cost 71 | self.cost_first_byte = Var(shape=shape, init=0) 72 | 73 | # Note: Total min cost = cost_min_first_byte << 24 + cost_min_last_bytes 74 | # Extract first 8 bit 75 | cost_min_first_byte = np.right_shift(min_cost, 24) 76 | cost_min_first_byte = max(-2 ** 7, min(cost_min_first_byte, 2 ** 7 - 1)) 77 | # Extract last 24 bit 78 | cost_min_last_bytes = min_cost & 2 ** 24 - 1 79 | # last 24 bit of cost 80 | self.cost_min_last_bytes = Var(shape=shape, init=cost_min_last_bytes) 81 | # first 8 bit of cost 82 | self.cost_min_first_byte = Var(shape=shape, init=cost_min_first_byte) 83 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/lca/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Battelle Memorial Institute 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | 7 | from lava.magma.core.model.sub.model import AbstractSubProcessModel 8 | 9 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 10 | from lava.magma.core.decorator import implements, tag 11 | from lava.proc.dense.process import Dense 12 | 13 | from lava.lib.optimization.solvers.lca.process import LCA1Layer, LCA2Layer 14 | from lava.lib.optimization.solvers.lca.v1_neuron.process import V1Neuron 15 | from lava.lib.optimization.solvers.lca.residual_neuron.process import \ 16 | ResidualNeuron 17 | 18 | 19 | @implements(proc=LCA2Layer, protocol=LoihiProtocol) 20 | class LCA2LayerModel(AbstractSubProcessModel): 21 | def __init__(self, proc: LCA2Layer): 22 | threshold = proc.threshold.get() 23 | T = proc.tau.get() 24 | T_exp = proc.tau_exp.get() 25 | weights = proc.weights.get() 26 | weights_exp = proc.weights_exp.get() 27 | input_val = proc.input.get() 28 | spike_height = proc.spike_height.get() 29 | 30 | self.v1 = V1Neuron(shape=(weights.shape[0],), tau=T, tau_exp=T_exp, 31 | vth=threshold, two_layer=True) 32 | # weight_exp shifted 8 bits for the weights, 6 for the v1 output. 33 | self.weights_T = Dense(weights=-weights.T, num_message_bits=24, 34 | weight_exp=weights_exp) 35 | 36 | self.res = ResidualNeuron(shape=(weights.shape[1],), 37 | spike_height=spike_height, bias=input_val) 38 | 39 | self.weights = Dense(weights=(weights * T), num_message_bits=24, 40 | weight_exp=weights_exp + T_exp) 41 | 42 | self.weights.a_out.connect(self.v1.a_in) 43 | self.res.s_out.connect(self.weights.s_in) 44 | 45 | self.weights_T.a_out.connect(self.res.a_in) 46 | self.v1.s_out.connect(self.weights_T.s_in) 47 | 48 | # Expose output and voltage 49 | self.v1.s_out.connect(proc.out_ports.v1) 50 | self.res.s_out.connect(proc.out_ports.res) 51 | proc.vars.voltage.alias(self.v1.vars.v) 52 | proc.vars.input.alias(self.res.bias) 53 | 54 | 55 | @implements(proc=LCA1Layer, protocol=LoihiProtocol) 56 | class LCA1LayerModel(AbstractSubProcessModel): 57 | def __init__(self, proc: LCA1Layer): 58 | threshold = proc.threshold.get() 59 | T = proc.tau.get() 60 | T_exp = proc.tau_exp.get() 61 | 62 | weights = proc.weights.get() 63 | weights_exp = proc.weights_exp.get() 64 | bias = proc.bias.get() 65 | 66 | self.v1 = V1Neuron(shape=(weights.shape[0],), tau=T, tau_exp=T_exp, 67 | vth=threshold, bias=bias, two_layer=False) 68 | # weight_exp shifted 8 bits for the weights, 6 for the v1 output. 69 | self.weights = Dense(weights=weights, num_message_bits=24, 70 | weight_exp=weights_exp) 71 | 72 | self.weights.a_out.connect(self.v1.a_in) 73 | self.v1.s_out.connect(self.weights.s_in) 74 | 75 | # Expose output and voltage 76 | self.v1.s_out.connect(proc.out_ports.v1) 77 | proc.vars.voltage.alias(self.v1.vars.v) 78 | proc.vars.bias.alias(self.v1.bias) 79 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/utils/generators/test_mis.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import unittest 6 | 7 | import numpy as np 8 | 9 | from lava.lib.optimization.utils.generators.mis import MISProblem 10 | 11 | 12 | class TestMISProblem(unittest.TestCase): 13 | """Unit tests for MISProblem class.""" 14 | 15 | def setUp(self): 16 | self.adjacency = np.array( 17 | [[0, 1, 1, 0], [1, 0, 0, 0], [1, 0, 0, 1], [0, 0, 1, 0]] 18 | ) 19 | self.num_vertices = self.adjacency.shape[0] 20 | self.num_edges = np.count_nonzero(self.adjacency) 21 | self.problem = MISProblem(adjacency_matrix=self.adjacency) 22 | 23 | def test_create_obj(self): 24 | """Tests correct instantiation of MISProblem object.""" 25 | self.assertIsInstance(self.problem, MISProblem) 26 | 27 | def test_num_vertices_property(self): 28 | """Tests correct value of num_vertices property.""" 29 | self.assertEqual(self.problem.num_vertices, self.num_vertices) 30 | 31 | def test_num_edges_property(self): 32 | """Tests correct value of num_edges property.""" 33 | self.assertEqual(self.problem.num_edges, self.num_edges) 34 | 35 | def test_adjacency_matrix_property(self): 36 | """Tests correct value of adjacency_matrix.""" 37 | self.assertTrue((self.adjacency == self.problem.adjacency_matrix).all()) 38 | 39 | def test_get_graph(self): 40 | """Tests that the graph contains the correct number of nodes and 41 | edges.""" 42 | graph = self.problem.get_graph() 43 | self.assertEqual(graph.number_of_nodes(), self.num_vertices) 44 | self.assertEqual(graph.number_of_edges() * 2, self.num_edges) 45 | 46 | def test_get_complement_graph(self): 47 | """Tests that the complement graph contains the correct number of 48 | nodes and edges.""" 49 | graph = self.problem.get_complement_graph() 50 | self.assertEqual(graph.number_of_nodes(), self.num_vertices) 51 | self.assertEqual(graph.number_of_edges() * 2, self.num_edges) 52 | 53 | def test_get_complement_graph_matrix(self): 54 | """Tests the correct complement graph adjacency matrix is returned.""" 55 | matrix = self.problem.get_complement_graph_matrix() 56 | correct_matrix = np.array( 57 | [[0, 0, 0, 1], [0, 0, 1, 1], [0, 1, 0, 0], [1, 1, 0, 0]] 58 | ) 59 | self.assertTrue((matrix == correct_matrix).all()) 60 | 61 | def test_get_as_qubo(self): 62 | """Tests the conversion to QUBO returns the correct cost matrix.""" 63 | w_diag = 1 64 | w_off = 4 65 | qubo = self.problem.get_as_qubo(w_diag, w_off) 66 | self.assertTrue(np.all(qubo.q[np.diag_indices(4)] == -w_diag)) 67 | self.assertTrue(np.all(qubo.q[qubo.q > 0] == w_off / 2)) 68 | self.assertTrue(np.all(qubo.q == qubo.q.T)) 69 | 70 | def test_find_maximum_independent_set(self): 71 | """Tests the correct maximum independent set is returned.""" 72 | mis = self.problem.find_maximum_independent_set() 73 | correct_set_size = 2 74 | self.assertEqual(mis.sum(), correct_set_size) 75 | 76 | 77 | if __name__ == "__main__": 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /tutorials/data/qubo/workloads/mis-uniform/gen_mis_uniform.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e81fe7c7-e7db-4d76-a211-365f79b4333f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import os\n", 11 | "import numpy as np\n", 12 | "import time\n", 13 | "import warnings\n", 14 | "from dataclasses import dataclass\n", 15 | "\n", 16 | "from lava.lib.optimization.utils.generators.mis import MISProblem\n", 17 | "\n", 18 | "warnings.filterwarnings('ignore')" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "c72fcc4d-3e04-49a3-8146-60d7414ddb97", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "size = 6000" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 12, 34 | "id": "e39da1a6-a016-4fa3-b8d8-b07ffadef557", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "q_saved = np.loadtxt(f'/home/sumedhrr/202312_qubo_benchmarking/lava-loihi-benchmarking/workloads/mis-uniform/mis-uniform-{size}-0.05-0.txt', dtype=int)" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 3, 44 | "id": "87bcaa20-fc8a-4170-8f24-fb0e412bcb9b", 45 | "metadata": {}, 46 | "outputs": [ 47 | { 48 | "name": "stdout", 49 | "output_type": "stream", 50 | "text": [ 51 | "Generating size=4450 seed=0\n", 52 | "Generating size=4450 seed=1\n", 53 | "Generating size=4450 seed=2\n", 54 | "Generating size=4450 seed=3\n", 55 | "Generating size=4450 seed=4\n" 56 | ] 57 | } 58 | ], 59 | "source": [ 60 | "for size in [4450]:\n", 61 | " for seed in range(5):\n", 62 | " np.random.seed(seed)\n", 63 | " print(f'Generating {size=} {seed=}')\n", 64 | " q_gened = MISProblem.from_random_uniform(num_vertices=size, density=0.05, seed=seed).adjacency_matrix\n", 65 | " np.savetxt(f'/home/sumedhrr/202312_qubo_benchmarking/lava-loihi-benchmarking/workloads/mis-uniform/mis-uniform-{size}-0.05-{seed}.txt', q_gened, fmt=\"%d\")" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 14, 71 | "id": "714d5d1b-24f3-4e80-b629-5064f9a33ab7", 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "data": { 76 | "text/plain": [ 77 | "False" 78 | ] 79 | }, 80 | "execution_count": 14, 81 | "metadata": {}, 82 | "output_type": "execute_result" 83 | } 84 | ], 85 | "source": [ 86 | "np.all(q_saved == q_gened)" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "id": "1bb825fc-8f3b-43bf-ae2f-23c375403610", 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [] 96 | } 97 | ], 98 | "metadata": { 99 | "kernelspec": { 100 | "display_name": "Python 3 (ipykernel)", 101 | "language": "python", 102 | "name": "python3" 103 | }, 104 | "language_info": { 105 | "codemirror_mode": { 106 | "name": "ipython", 107 | "version": 3 108 | }, 109 | "file_extension": ".py", 110 | "mimetype": "text/x-python", 111 | "name": "python", 112 | "nbconvert_exporter": "python", 113 | "pygments_lexer": "ipython3", 114 | "version": "3.8.10" 115 | } 116 | }, 117 | "nbformat": 4, 118 | "nbformat_minor": 5 119 | } 120 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/utils/test_report_analyzer.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import os 6 | import unittest 7 | 8 | import numpy as np 9 | from lava.lib.optimization.problems.problems import QUBO 10 | from lava.lib.optimization.solvers.generic.solver import SolverReport 11 | from lava.lib.optimization.utils.report_analyzer import ReportAnalyzer 12 | 13 | 14 | def prepare_problem_and_report(): 15 | np.random.seed(0) 16 | size = 5 17 | timeout = 100 18 | q = np.random.randint(0, 20, size=(size, size), dtype=np.int32) 19 | q_symm = ((q + q.T) / 2).astype(int) 20 | problem = QUBO(q=q_symm) 21 | states = np.random.randint(0, 2, size=(timeout, size), dtype=np.int32) 22 | costs = list(map(problem.evaluate_cost, states)) 23 | report = SolverReport( 24 | problem=problem, cost_timeseries=costs, state_timeseries=states 25 | ) 26 | return report 27 | 28 | 29 | class TestReportAnalyzer(unittest.TestCase): 30 | def setUp(self) -> None: 31 | self.report = prepare_problem_and_report() 32 | self.analysis = ReportAnalyzer(report=self.report) 33 | 34 | def test_create_obj(self) -> None: 35 | self.assertIsInstance(self.analysis, ReportAnalyzer) 36 | 37 | def test_plot_cost_timeseries(self) -> None: 38 | filename = "plot_cost_timeseries.png" 39 | self.analysis.plot_cost_timeseries(filename=filename) 40 | self.assertTrue(os.path.exists(filename)) 41 | os.remove(filename) 42 | 43 | def test_plot_min_cost_timeseries(self) -> None: 44 | filename = "plot_min_cost_timseries.png" 45 | self.analysis.plot_min_cost_timeseries(filename=filename) 46 | self.assertTrue(os.path.exists(filename)) 47 | os.remove(filename) 48 | 49 | def test_plot_cost_distribution(self) -> None: 50 | filename = "plot_cost_distribution.png" 51 | self.analysis.plot_cost_distribution(filename=filename) 52 | self.assertTrue(os.path.exists(filename)) 53 | os.remove(filename) 54 | 55 | def test_plot_delta_cost_distribution(self) -> None: 56 | filename = "plot_delta_cost_distribution.png" 57 | self.analysis.plot_delta_cost_distribution(filename=filename) 58 | self.assertTrue(os.path.exists(filename)) 59 | os.remove(filename) 60 | 61 | def test_plot_num_visited_states(self) -> None: 62 | filename = "plot_num_visited_states.png" 63 | self.analysis.plot_num_visited_states(filename=filename) 64 | self.assertTrue(os.path.exists(filename)) 65 | os.remove(filename) 66 | 67 | def test_plot_successive_states_distance(self) -> None: 68 | filename = "plot_successive_states_distance.png" 69 | self.analysis.plot_successive_states_distance(filename=filename) 70 | self.assertTrue(os.path.exists(filename)) 71 | os.remove(filename) 72 | 73 | def test_plot_state_timeseries(self) -> None: 74 | filename = "plot_state_timeseries.png" 75 | self.analysis.plot_state_timeseries(filename=filename) 76 | self.assertTrue(os.path.exists(filename)) 77 | os.remove(filename) 78 | 79 | def test_plot_state_analysis_summary(self) -> None: 80 | filename = "plot_state_analysis_summary.png" 81 | self.analysis.plot_state_analysis_summary(filename=filename) 82 | self.assertTrue(os.path.exists(filename)) 83 | os.remove(filename) 84 | 85 | 86 | if __name__ == "__main__": 87 | unittest.main() 88 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/problems/test_cost.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | 6 | import unittest 7 | 8 | import numpy as np 9 | 10 | from lava.lib.optimization.problems.coefficients import CoefficientTensorsMixin 11 | from lava.lib.optimization.problems.cost import Cost 12 | 13 | 14 | class TestCost(unittest.TestCase): 15 | def setUp(self) -> None: 16 | self.raw_coefficients = ( 17 | np.asarray(1), 18 | np.ones(2), 19 | np.ones((2, 2)), 20 | np.ones((2, 2, 2)), 21 | ) 22 | self.augmented_terms = ( 23 | np.asarray(5), 24 | 5 * np.ones(2), 25 | 5 * np.ones((2, 2)), 26 | 5 * np.ones((2, 2, 2)), 27 | ) 28 | self.cost = Cost(*self.raw_coefficients) 29 | self.cost_augmented = Cost(*self.raw_coefficients, 30 | augmented_terms=self.augmented_terms) 31 | 32 | def test_create_obj(self): 33 | for cost, label in [ 34 | (self.cost, "not augmented"), 35 | (self.cost_augmented, "augmented"), 36 | ]: 37 | with self.subTest(msg=f"{label}"): 38 | self.assertIsInstance(cost, Cost) 39 | 40 | def test_created_obj_includes_mixin(self): 41 | for cost, label in [ 42 | (self.cost, "not augmented"), 43 | (self.cost_augmented, "augmented"), 44 | ]: 45 | with self.subTest(msg=f"{label}"): 46 | self.assertIsInstance(cost, CoefficientTensorsMixin) 47 | 48 | def test_coefficients(self): 49 | for n, coefficient in enumerate(self.raw_coefficients): 50 | with self.subTest(msg=f"{n}"): 51 | self.assertTrue( 52 | (coefficient == self.cost.coefficients[n]).all() 53 | ) 54 | 55 | def test_is_augmented(self): 56 | self.assertTrue(self.cost_augmented.is_augmented) 57 | 58 | def test_is_not_augmented(self): 59 | self.assertFalse(self.cost.is_augmented) 60 | 61 | def test_augmented_terms(self): 62 | for n, coefficient in enumerate(self.augmented_terms): 63 | with self.subTest(msg=f"{n}"): 64 | aug_terms = self.cost_augmented.augmented_terms 65 | self.assertTrue((coefficient == aug_terms[n]).all()) 66 | 67 | def test_reset_augmented_terms(self): 68 | new_augmented_terms = ( 69 | np.asarray(3), 70 | 3 * np.ones(2), 71 | 3 * np.ones((2, 2)), 72 | 3 * np.ones((2, 2, 2)), 73 | ) 74 | self.cost_augmented.augmented_terms = new_augmented_terms 75 | for n, coefficient in enumerate(new_augmented_terms): 76 | with self.subTest(msg=f"{n}"): 77 | aug_terms = self.cost_augmented.augmented_terms 78 | self.assertTrue((coefficient == aug_terms[n]).all()) 79 | 80 | def test_set_augmented_terms(self): 81 | new_augmented_terms = ( 82 | np.asarray(3), 83 | 3 * np.ones(2), 84 | 3 * np.ones((2, 2)), 85 | 3 * np.ones((2, 2, 2)), 86 | ) 87 | self.cost.augmented_terms = new_augmented_terms 88 | for n, coefficient in enumerate(new_augmented_terms): 89 | with self.subTest(msg=f"{n}"): 90 | self.assertTrue( 91 | (coefficient == self.cost.augmented_terms[n]).all() 92 | ) 93 | 94 | def test_augmented_terms_defaults_to_none(self): 95 | self.assertIsNone(self.cost.augmented_terms) 96 | 97 | 98 | if __name__ == "__main__": 99 | unittest.main() 100 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/clustering/utils/test_q_matrix_generator.py: -------------------------------------------------------------------------------- 1 | # INTEL CORPORATION CONFIDENTIAL AND PROPRIETARY 2 | # 3 | # Copyright © 2023 Intel Corporation. 4 | # 5 | # This software and the related documents are Intel copyrighted 6 | # materials, and your use of them is governed by the express 7 | # license under which they were provided to you (License). Unless 8 | # the License provides otherwise, you may not use, modify, copy, 9 | # publish, distribute, disclose or transmit this software or the 10 | # related documents without Intel's prior written permission. 11 | # 12 | # This software and the related documents are provided as is, with 13 | # no express or implied warranties, other than those that are 14 | # expressly stated in the License. 15 | import unittest 16 | import numpy as np 17 | from scipy.spatial import distance 18 | from lava.lib.optimization.apps.clustering.utils.q_matrix_generator import \ 19 | QMatrixClust 20 | 21 | 22 | class TestMatrixGen(unittest.TestCase): 23 | def test_Q_gen(self) -> None: 24 | input_nodes = np.array([(0, 1), (2, 3), (2, 1), (1, 1)]) 25 | lambda_centers = 1.5 26 | lambda_points = 2.5 27 | lambda_dist = 2 28 | num_clusters = 2 29 | # testing floating point Q matrix 30 | q_obj = QMatrixClust( 31 | input_nodes, 32 | num_clusters=num_clusters, 33 | lambda_dist=lambda_dist, 34 | lambda_centers=lambda_centers, 35 | lambda_points=lambda_points, 36 | ) 37 | Q_clustering_fltg_pt = q_obj.matrix 38 | Q_dist_test = distance.cdist(input_nodes, 39 | input_nodes, 40 | "euclidean") 41 | Q_dist_blck_test = np.kron(np.eye(num_clusters), Q_dist_test) 42 | 43 | Q_wypt_cnst_test = np.array( 44 | [ 45 | [-1., 0., 0., 0., 2., 0., 0., 0.], 46 | [0., -1., 0., 0., 0., 2., 0., 0.], 47 | [0., 0., -1., 0., 0., 0., 2., 0.], 48 | [0., 0., 0., -1., 0., 0., 0., 2.], 49 | [2., 0., 0., 0., -1., 0., 0., 0.], 50 | [0., 2., 0., 0., 0., -1., 0., 0.], 51 | [0., 0., 2., 0., 0., 0., -1., 0.], 52 | [0., 0., 0., 2., 0., 0., 0., -1.] 53 | ] 54 | ) 55 | 56 | Q_vhcle_cnst_test = np.array( 57 | [ 58 | [-1.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 59 | [2.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 60 | [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 61 | [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 62 | [0.0, 0.0, 0.0, 0.0, -1.0, 2.0, 0.0, 0.0], 63 | [0.0, 0.0, 0.0, 0.0, 2.0, -1.0, 0.0, 0.0], 64 | [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 65 | [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 66 | ] 67 | ) 68 | Q_test = ( 69 | lambda_dist * Q_dist_blck_test 70 | + lambda_centers * Q_vhcle_cnst_test 71 | + lambda_points * Q_wypt_cnst_test 72 | ) 73 | self.assertEqual(np.all(Q_clustering_fltg_pt == Q_test), True) 74 | 75 | # testing fixed point Q matrix 76 | # individual values not really testsed since the rounding 77 | # is stochastic 78 | q_obj_2 = QMatrixClust( 79 | input_nodes, 80 | num_clusters=2, 81 | fixed_pt=True, 82 | ) 83 | Q_clustering_fixed_pt = q_obj_2.matrix 84 | fixed_pt_Q_max = np.max(np.abs(Q_clustering_fixed_pt)) 85 | fixed_pt_Q_min = np.min(np.abs(Q_clustering_fixed_pt)) 86 | self.assertGreaterEqual(fixed_pt_Q_min == 0, True) 87 | self.assertLessEqual(fixed_pt_Q_max == 127, True) 88 | 89 | 90 | if __name__ == "__main__": 91 | unittest.main() 92 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/qp/solver.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021-2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | from lava.magma.core.run_conditions import RunSteps 7 | from lava.magma.core.run_configs import Loihi1SimCfg 8 | from lava.lib.optimization.problems.problems import QP 9 | from lava.lib.optimization.solvers.generic.qp.models import ( 10 | ConstraintCheck, 11 | GradientDynamics, 12 | ) 13 | 14 | 15 | # Future: inheritance from OptimizationSolver class 16 | class QPSolver: 17 | """Solve Full QP by connecting two Lava processes, GradDynamics and 18 | ConstraintCheck 19 | 20 | Parameters 21 | ---------- 22 | 23 | alpha : 1-D np.array 24 | The learning rate for gradient descent 25 | beta : 1-D np.array 26 | The learning rate for constraint correction 27 | alpha_decay_schedule : int, default 10000 28 | Number of iterations after which one right shift takes place for 29 | alpha 30 | beta_growth_schedule : int, default 10000 31 | Number of iterations after which one left shift takes place for 32 | beta 33 | 34 | """ 35 | 36 | def __init__( 37 | self, 38 | alpha: np.ndarray, 39 | beta: np.ndarray, 40 | alpha_decay_schedule: int, 41 | beta_growth_schedule: int, 42 | ): 43 | self.alpha = alpha 44 | self.beta = beta 45 | self.beta_g = beta_growth_schedule 46 | self.alpha_d = alpha_decay_schedule 47 | 48 | def solve(self, problem: QP, iterations: int = 400): 49 | """solves the supplied QP problem 50 | 51 | Parameters 52 | ---------- 53 | problem : QP 54 | The QP containing the matrices that set up the problem 55 | iterations : int, optional 56 | Number of iterations for which QP has to run, by default 400 57 | 58 | Returns 59 | -------- 60 | sol : 1-D np.array 61 | Solution to the quadratic program 62 | """ 63 | ( 64 | hessian, 65 | linear_offset, 66 | ) = (problem.get_hessian, problem.get_linear_offset) 67 | if problem.get_constraint_hyperplanes is not None: 68 | constraint_hyperplanes, constraint_biases = ( 69 | problem.get_constraint_hyperplanes, 70 | problem.get_constraint_biases, 71 | ) 72 | 73 | else: 74 | constraint_hyperplanes, constraint_biases = np.zeros( 75 | (hessian.shape[0], hessian.shape[1]) 76 | ), np.zeros((hessian.shape[0], 1)) 77 | 78 | init_sol = np.random.rand(hessian.shape[0], 1) 79 | i_max = iterations 80 | ConsCheck = ConstraintCheck( 81 | constraint_matrix=constraint_hyperplanes, 82 | constraint_bias=constraint_biases, 83 | ) 84 | GradDyn = GradientDynamics( 85 | hessian=hessian, 86 | constraint_matrix_T=constraint_hyperplanes.T, 87 | qp_neurons_init=init_sol, 88 | grad_bias=linear_offset, 89 | alpha=self.alpha, 90 | beta=self.beta, 91 | alpha_decay_schedule=self.alpha_d, 92 | beta_growth_schedule=self.beta_g, 93 | ) 94 | 95 | # core solver 96 | GradDyn.s_out.connect(ConsCheck.s_in) 97 | ConsCheck.s_out.connect(GradDyn.s_in) 98 | 99 | GradDyn.run( 100 | condition=RunSteps(num_steps=i_max), 101 | run_cfg=Loihi1SimCfg(select_sub_proc_model=True), 102 | ) 103 | sol = GradDyn.vars.qp_neuron_state.get() 104 | GradDyn.stop() 105 | return sol 106 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/problems/variables.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import typing as ty 6 | 7 | DType = ty.Union[ty.List[int], ty.List[ty.Tuple[ty.Any]]] 8 | 9 | 10 | class Variable: 11 | """An entity to which a value can be assigned. 12 | 13 | Parameters 14 | ---------- 15 | name: Optional name for the variable. 16 | """ 17 | 18 | def __init__(self, name: str = None): 19 | self.name = name 20 | self._value = None 21 | 22 | @property 23 | def value(self): 24 | """Variable's current value.""" 25 | return self._value 26 | 27 | @value.setter 28 | def value(self, value): 29 | self._value = value 30 | 31 | 32 | class DiscreteVariables: 33 | """A set of variables which can only be assigned discrete values. 34 | 35 | Parameters 36 | ---------- 37 | domains: List of tuples with values that each variable can take or List 38 | of domain sizes for each corresponding (by index) variable. 39 | """ 40 | 41 | def __init__(self, domains: DType = None): 42 | self._domains = domains 43 | 44 | @property 45 | def domain_sizes(self): 46 | """Number of elements on the domain of each discrete variable.""" 47 | return [len(d) for d in self.domains] 48 | 49 | @property 50 | def domains(self): 51 | """List of tuples containing the values that each variable can take.""" 52 | if self._domains is None: 53 | return None 54 | if type(self._domains[0]) is int: 55 | return [range(d) for d in self._domains] 56 | elif type(self._domains[0]) is tuple: 57 | return self._domains 58 | else: 59 | raise ValueError("Domains format not recognized.") 60 | 61 | @property 62 | def variable_set(self): 63 | """List of discrete variables each an instance of the Variable class.""" 64 | return [Variable(name=str(n)) for n in range(self.num_variables)] 65 | 66 | @property 67 | def num_variables(self): 68 | """Number of variables in this set.""" 69 | return len(self.domains) if self.domains is not None else None 70 | 71 | @domains.setter 72 | def domains(self, value: DType): 73 | self._domains = value 74 | 75 | 76 | class ContinuousVariables: 77 | """Set of variables to which any values in specified ranges can be assigned. 78 | 79 | Parameters 80 | ---------- 81 | bounds: List of 2-tuples defining the range from which each corresponding 82 | variable (by index) can take values. 83 | """ 84 | 85 | def __init__(self, num_variables=None, bounds: ty.List[ty.Tuple] = None): 86 | self._num_variables = num_variables 87 | self._bounds = bounds 88 | 89 | @property 90 | def variable_set(self): 91 | """List of continuous variables as instances of the Variable class.""" 92 | return [Variable(name=str(n)) for n in range(self.num_variables)] 93 | 94 | @property 95 | def num_variables(self): 96 | """Number of variables in this set.""" 97 | return self._num_variables 98 | 99 | @property 100 | def bounds(self): 101 | """Limit values defining the ranges of allowed values for variables.""" 102 | return self._bounds 103 | 104 | @bounds.setter 105 | def bounds(self, value: ty.List[ty.Tuple]): 106 | self._bounds = value 107 | 108 | 109 | class Variables: 110 | def __init__(self): 111 | self._discrete = DiscreteVariables() 112 | self._continuous = ContinuousVariables() 113 | 114 | @property 115 | def continuous(self): 116 | return self._continuous 117 | 118 | @continuous.setter 119 | def continuous(self, value): 120 | self._continuous = value 121 | 122 | @property 123 | def discrete(self): 124 | return self._discrete 125 | 126 | @discrete.setter 127 | def discrete(self, value): 128 | self._discrete = value 129 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/apps/schduler/test_solver.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import pprint 5 | import unittest 6 | import os 7 | 8 | import numpy as np 9 | 10 | from lava.lib.optimization.apps.scheduler.problems import ( 11 | SchedulingProblem, SatelliteScheduleProblem) 12 | from lava.lib.optimization.apps.scheduler.solver import (Scheduler, 13 | SatelliteScheduler) 14 | 15 | 16 | def get_bool_env_setting(env_var: str): 17 | """Get an environment variable and return True if the variable is set to 18 | 1 else return false. 19 | """ 20 | env_test_setting = os.environ.get(env_var) 21 | test_setting = False 22 | if env_test_setting == "1": 23 | test_setting = True 24 | return test_setting 25 | 26 | 27 | run_loihi_tests: bool = get_bool_env_setting("RUN_LOIHI_TESTS") 28 | run_lib_tests: bool = get_bool_env_setting("RUN_LIB_TESTS") 29 | skip_reason = "Either Loihi tests or Lib tests or both are not enabled." 30 | 31 | 32 | class TestScheduler(unittest.TestCase): 33 | def setUp(self) -> None: 34 | self.sp = SchedulingProblem(num_agents=3, num_tasks=3) 35 | self.sp.generate(seed=42) 36 | self.scheduler = Scheduler(sp=self.sp, qubo_weights=(4, 20)) 37 | 38 | def test_init(self): 39 | self.assertIsInstance(self.scheduler, Scheduler) # add assertion here 40 | 41 | @unittest.skipUnless(run_lib_tests and run_loihi_tests, skip_reason) 42 | def test_netx_solver(self): 43 | self.scheduler.solve_with_netx() 44 | gt_sol = np.array([[0., 0., 0., 0.], 45 | [5., 1., 2., 2.], 46 | [7., 2., 1., 1.]]) 47 | self.assertTrue(np.all(self.scheduler.netx_solution == gt_sol)) 48 | 49 | @unittest.skipUnless(run_lib_tests and run_loihi_tests, skip_reason) 50 | def test_lava_solver(self): 51 | self.scheduler.solve_with_lava_qubo() 52 | gt_possible_node_ids = [[0, 4, 8], [0, 5, 7], 53 | [1, 3, 8], [1, 5, 6], 54 | [2, 3, 7], [2, 4, 6]] 55 | self.assertTrue(self.scheduler.lava_solution[:, 0].tolist() in 56 | gt_possible_node_ids) 57 | 58 | 59 | class TestSatelliteScheduler(unittest.TestCase): 60 | def setUp(self) -> None: 61 | requests = np.array( 62 | [[0.02058449, 0.96990985], [0.05808361, 0.86617615], 63 | [0.15601864, 0.15599452], [0.18182497, 0.18340451], 64 | [0.29214465, 0.36636184], [0.30424224, 0.52475643], 65 | [0.37454012, 0.95071431], [0.43194502, 0.29122914], 66 | [0.60111501, 0.70807258], [0.61185289, 0.13949386], 67 | [0.73199394, 0.59865848], [0.83244264, 0.21233911]] 68 | ) 69 | self.ssp = SatelliteScheduleProblem(num_satellites=3, 70 | num_requests=12, 71 | requests=requests) 72 | self.ssp.generate(seed=42) 73 | self.sat_scheduler = SatelliteScheduler(ssp=self.ssp, 74 | qubo_weights=(4, 20)) 75 | self.gt_sol = np.array([[0., 1., 0.30424224, 0.52475643], 76 | [1., 1., 0.73199394, 0.59865848], 77 | [2., 2., 0.02058449, 0.96990985], 78 | [3., 2., 0.37454012, 0.95071431]]) 79 | 80 | def test_init(self): 81 | self.assertIsInstance(self.sat_scheduler, SatelliteScheduler) 82 | 83 | @unittest.skipUnless(run_lib_tests and run_loihi_tests, skip_reason) 84 | def test_netx_solver(self): 85 | self.sat_scheduler.solve_with_netx() 86 | self.assertTrue(np.all(self.sat_scheduler.netx_solution == self.gt_sol)) 87 | 88 | @unittest.skipUnless(run_lib_tests and run_loihi_tests, skip_reason) 89 | def test_lava_solver(self): 90 | self.sat_scheduler.solve_with_lava_qubo() 91 | self.assertTrue(np.all(self.sat_scheduler.lava_solution == self.gt_sol)) 92 | 93 | 94 | if __name__ == '__main__': 95 | unittest.main() 96 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/nebm/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022-2024 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | 7 | from lava.lib.optimization.solvers.generic.nebm.process import NEBM 8 | from lava.magma.core.decorator import implements, requires, tag 9 | from lava.magma.core.model.py.model import PyLoihiProcessModel 10 | from lava.magma.core.model.py.ports import PyInPort, PyOutPort 11 | from lava.magma.core.model.py.type import LavaPyType 12 | from lava.magma.core.resources import CPU 13 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 14 | 15 | 16 | def boltzmann(delta_e, temperature): 17 | """ Return the probability that each unit should be ON from a boltzmann 18 | distribution with the given temperature. Note that the boltzmann 19 | distribution is undefined for temperature equal to zero, in this case 20 | return probabilities corresponding to greedy energy descent.""" 21 | if temperature == 0: 22 | return delta_e < 0 23 | return 1 / (1 + np.exp(delta_e / temperature)) 24 | 25 | 26 | @implements(proc=NEBM, protocol=LoihiProtocol) 27 | @requires(CPU) 28 | @tag('fixed_pt') 29 | class NEBMPyModel(PyLoihiProcessModel): 30 | """ 31 | Fixed point implementation of Boltzmann neuron for solving QUBO problems. 32 | """ 33 | a_in = LavaPyType(PyInPort.VEC_DENSE, int, precision=24) 34 | s_sig_out = LavaPyType(PyOutPort.VEC_DENSE, int, precision=24) 35 | s_wta_out = LavaPyType(PyOutPort.VEC_DENSE, int, precision=24) 36 | state: np.ndarray = LavaPyType(np.ndarray, int, precision=24) 37 | spk_hist: np.ndarray = LavaPyType(np.ndarray, int, precision=8) 38 | temperature: np.ndarray = LavaPyType(np.ndarray, int, precision=8) 39 | refract: np.ndarray = LavaPyType(np.ndarray, int, precision=8) 40 | refract_counter: np.ndarray = LavaPyType(np.ndarray, int, precision=8) 41 | 42 | def __init__(self, proc_params): 43 | super().__init__(proc_params) 44 | self.a_in_data = np.zeros(proc_params['shape']) 45 | 46 | def _update_buffers(self): 47 | # Populate the buffer for local computation 48 | spk_hist_buffer = self.spk_hist.copy() 49 | spk_hist_buffer &= 3 50 | self.spk_hist <<= 2 51 | self.spk_hist &= 0xFF # AND with 0xFF retains 8 LSBs 52 | return spk_hist_buffer 53 | 54 | def _generate_sigma_spikes(self, spk_hist_buffer): 55 | s_sig = np.zeros_like(self.state) 56 | # If we have fired in the previous time-step, we send out the local 57 | # cost now, i.e., when spk_hist_buffer == 1 58 | sig_spk_idx = np.where(spk_hist_buffer == 1) 59 | # Send the local cost 60 | s_sig[sig_spk_idx] = self.state[sig_spk_idx] 61 | return s_sig 62 | 63 | def _generate_wta_spikes(self, spk_hist_buffer): 64 | self.state += self.a_in_data 65 | wta_spk_idx_prev = (spk_hist_buffer == 1) 66 | # HACK: Need to enable temperature < 1, because temperature = 1 is 67 | # already too hot for small QUBOs. 68 | prob = boltzmann(self.state, self.temperature[0] / 4.0) 69 | rand = np.random.rand(*prob.shape) 70 | wta_spk_idx = rand < prob 71 | refractory = self.refract_counter > 0 72 | wta_spk_idx[refractory] = wta_spk_idx_prev[refractory] 73 | # Add spikes to history 74 | self.spk_hist[wta_spk_idx] |= 1 75 | # Generate output spikes for feedback 76 | s_wta = np.zeros_like(self.state) 77 | pos_spk = np.logical_and(wta_spk_idx, np.logical_not(wta_spk_idx_prev)) 78 | neg_spk = np.logical_and(wta_spk_idx_prev, np.logical_not(wta_spk_idx)) 79 | s_wta[pos_spk] = +1 80 | s_wta[neg_spk] = -1 81 | # Update the refractory periods 82 | self.refract_counter = np.maximum( 83 | self.refract_counter - 1, 84 | np.multiply(self.refract, (s_wta != 0))) 85 | return s_wta 86 | 87 | def run_spk(self) -> None: 88 | self.a_in_data = self.a_in.recv().astype(int) 89 | spk_hist_buffer = self._update_buffers() 90 | s_wta = self._generate_wta_spikes(spk_hist_buffer) 91 | s_sig = self._generate_sigma_spikes(spk_hist_buffer) 92 | self.s_wta_out.send(s_wta) 93 | self.s_sig_out.send(s_sig) 94 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/problems/bayesian/test_processes.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: LGPL-2.1-or-later 3 | # See: https://spdx.org/licenses/ 4 | 5 | import unittest 6 | 7 | from lava.lib.optimization.problems.bayesian.processes import ( 8 | BaseObjectiveFunction, 9 | SingleInputFunction, 10 | DualInputFunction, 11 | ) 12 | 13 | 14 | class TestProcesses(unittest.TestCase): 15 | """Initialization Tests of all processes of the Bayesian problems 16 | 17 | All tests check if the vars are properly assigned in the process and if 18 | the ports are shaped properly. Refer to Bayesian models.py to understand 19 | behaviors. 20 | """ 21 | 22 | def test_process_base_objective_func(self) -> None: 23 | """test initialization of the BaseObjectiveFunction process""" 24 | 25 | num_params: int = 5 26 | num_objectives: int = 2 27 | 28 | process = BaseObjectiveFunction( 29 | num_params=num_params, 30 | num_objectives=num_objectives 31 | ) 32 | 33 | # checking shape of input and output ports 34 | self.assertEqual( 35 | process.x_in.shape, 36 | (num_params, 1) 37 | ) 38 | self.assertEqual( 39 | process.y_out.shape, 40 | (num_params + num_objectives, 1) 41 | ) 42 | 43 | # checking shape of internal variables 44 | self.assertEqual( 45 | process.vars.num_params.shape, 46 | (1,) 47 | ) 48 | self.assertEqual( 49 | process.vars.num_objectives.shape, 50 | (1,) 51 | ) 52 | 53 | # checking values of internal variables 54 | self.assertEqual( 55 | process.vars.num_params.get(), 56 | num_params 57 | ) 58 | self.assertEqual( 59 | process.vars.num_objectives.get(), 60 | num_objectives 61 | ) 62 | 63 | def test_process_single_input_nonlinear_func(self) -> None: 64 | """test initialization of the SingleInputNonLinearFunction process""" 65 | 66 | num_params: int = 1 67 | num_objectives: int = 1 68 | 69 | process = SingleInputFunction() 70 | 71 | # checking shape of input and output ports 72 | self.assertEqual( 73 | process.x_in.shape, 74 | (num_params, 1) 75 | ) 76 | self.assertEqual( 77 | process.y_out.shape, 78 | (num_params + num_objectives, 1) 79 | ) 80 | 81 | # checking shape of internal variables 82 | self.assertEqual( 83 | process.vars.num_params.shape, 84 | (1,) 85 | ) 86 | self.assertEqual( 87 | process.vars.num_objectives.shape, 88 | (1,) 89 | ) 90 | 91 | # checking values of internal variables 92 | self.assertEqual( 93 | process.vars.num_params.get(), 94 | num_params 95 | ) 96 | self.assertEqual( 97 | process.vars.num_objectives.get(), 98 | num_objectives 99 | ) 100 | 101 | def test_process_dual_cont_input_func(self) -> None: 102 | """test initialization of the DualContInputFunction process""" 103 | 104 | num_params: int = 2 105 | num_objectives: int = 1 106 | 107 | process = DualInputFunction() 108 | 109 | # checking shape of input and output ports 110 | self.assertEqual( 111 | process.x_in.shape, 112 | (num_params, 1) 113 | ) 114 | self.assertEqual( 115 | process.y_out.shape, 116 | (num_params + num_objectives, 1) 117 | ) 118 | 119 | # checking shape of internal variables 120 | self.assertEqual( 121 | process.vars.num_params.shape, 122 | (1,) 123 | ) 124 | self.assertEqual( 125 | process.vars.num_objectives.shape, 126 | (1,) 127 | ) 128 | 129 | # checking values of internal variables 130 | self.assertEqual( 131 | process.vars.num_params.get(), 132 | num_params 133 | ) 134 | self.assertEqual( 135 | process.vars.num_objectives.get(), 136 | num_objectives 137 | ) 138 | 139 | 140 | if __name__ == "__main__": 141 | unittest.main() 142 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/lca/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Battelle Memorial Institute 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import typing as ty 6 | 7 | import numpy as np 8 | from lava.magma.core.process.process import AbstractProcess 9 | from lava.magma.core.process.variable import Var 10 | from lava.magma.core.process.ports.ports import OutPort 11 | 12 | 13 | class LCA1Layer(AbstractProcess): 14 | """Implements LCA based on https://doi.org/10.1162/neco.2008.03-07-486 15 | LCA minimizes sparse coding |a|_1 such that Φa ≈ s for some dictionary 'Φ' 16 | and vector to reconstruct 's' by using a set of neurons with voltage v and 17 | the dynamics below. As LCA accounts for the response properties of V1 simple 18 | cells, we refer to these neurons as v1. 19 | 20 | V1 Dynamics 21 | ----------- 22 | dv = -v + a(-Φ^T*Φ+I) + Φ^T*s 23 | a = soft_threshold(v) 24 | 25 | Parameters 26 | ---------- 27 | weights: Excitation / Inhibition weights mantissa - Φ^T*Φ-I * tau 28 | bias: Neuron bias - Φ^T*s 29 | weights_exp: Weights exponent 30 | threshold: neuron activation threshold 31 | tau: time constant mantissa 32 | tau_exp: time constant exponent 33 | """ 34 | 35 | def __init__( 36 | self, 37 | weights: np.ndarray, 38 | bias: np.ndarray, 39 | weights_exp: ty.Optional[int] = 0, 40 | threshold: ty.Optional[float] = 1, 41 | tau: ty.Optional[float] = 0.1, 42 | tau_exp: ty.Optional[int] = 0, 43 | **kwargs) -> None: 44 | super().__init__(**kwargs) 45 | 46 | self.threshold = Var(shape=(1,), init=threshold) 47 | self.tau = Var(shape=(1,), init=tau) 48 | self.tau_exp = Var(shape=(1,), init=tau_exp) 49 | self.weights = Var(shape=weights.shape, init=weights) 50 | self.weights_exp = Var(shape=(1,), init=weights_exp) 51 | self.v1 = OutPort(shape=(weights.shape[0],)) 52 | self.voltage = Var(shape=(weights.shape[0],)) 53 | self.bias = Var(shape=bias.shape, init=bias) 54 | 55 | 56 | class LCA2Layer(AbstractProcess): 57 | """Implements of 2-Layer LCA based on https://arxiv.org/abs/2205.15386 58 | LCA minimizes sparse coding |a|_1 such that Φa ≈ s for some dictionary 'Φ' 59 | and vector to reconstruct 's'. In two layer LCA, the reconstruction error Φa 60 | is separated out into its own layer r=s-Φa. This residual is made spiking by 61 | accumulating the error and spiking if it exceeds some spike_height. 62 | 63 | V1 Layer Dynamics 64 | ----------------- 65 | dv = -v + Φ^T(R_spike) + a 66 | a = soft_threshold(v) 67 | 68 | Residual Layer Dynamics 69 | ----------------------- 70 | r += s-Φa 71 | if |r| > spike_height 72 | R_spike = r 73 | r = 0 74 | 75 | Parameters 76 | ---------- 77 | weights: Excitation / Inhibition weights mantissa - Φ 78 | input_vec: Input to reconstruct - s 79 | weights_exp: Weights exponent 80 | input_exp: Input exponent 81 | threshold: Neuron activation threshold 82 | tau: Time constant mantissa 83 | tau_exp: Time constant exponent 84 | spike_height: Accumulator spike height 85 | """ 86 | 87 | def __init__( 88 | self, 89 | weights: np.ndarray, 90 | input_vec: np.ndarray, 91 | weights_exp: ty.Optional[int] = 0, 92 | input_exp: ty.Optional[int] = 0, 93 | threshold: ty.Optional[float] = 1, 94 | tau: ty.Optional[float] = 0.1, 95 | tau_exp: ty.Optional[int] = 0, 96 | spike_height: ty.Optional[int] = 1, 97 | **kwargs) -> None: 98 | super().__init__(**kwargs) 99 | 100 | self.threshold = Var(shape=(1,), init=threshold) 101 | self.tau = Var(shape=(1,), init=tau) 102 | self.tau_exp = Var(shape=(1,), init=tau_exp) 103 | self.weights = Var(shape=weights.shape, init=weights) 104 | self.weights_exp = Var(shape=(1,), init=weights_exp) 105 | self.v1 = OutPort(shape=(weights.shape[0],)) 106 | self.res = OutPort(shape=(weights.shape[1],)) 107 | self.voltage = Var(shape=(weights.shape[0],)) 108 | self.spike_height = Var(shape=(1,), init=spike_height) 109 | self.input = Var(shape=input_vec.shape, init=input_vec) 110 | self.input_exp = Var(shape=input_vec.shape, init=input_exp) 111 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/solution_finder/test_solution_finder.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import unittest 5 | import os 6 | 7 | import numpy as np 8 | from lava.lib.optimization.problems.problems import QP 9 | from lava.lib.optimization.solvers.generic.read_gate.models import ( 10 | get_read_gate_model_class, 11 | ) 12 | from lava.lib.optimization.solvers.generic.read_gate.process import ReadGate 13 | from lava.lib.optimization.solvers.generic.solution_finder.process import ( 14 | SolutionFinder, 15 | ) 16 | from lava.magma.core.run_conditions import RunSteps 17 | from lava.magma.core.run_configs import Loihi2SimCfg 18 | 19 | 20 | class TestSolutionFinder(unittest.TestCase): 21 | def setUp(self) -> None: 22 | root = os.path.dirname(os.path.abspath(__file__)) 23 | root = os.path.join(root, os.pardir) 24 | qp_data = np.load(root + "/data/qp/ex_qp_small.npz") 25 | Q, self.A, p, k = [qp_data[i] for i in qp_data] 26 | p, self.k = np.squeeze(p), np.squeeze(k) 27 | self.problem = QP( 28 | hessian=Q, 29 | linear_offset=p, 30 | equality_constraints_weights=self.A, 31 | equality_constraints_biases=self.k, 32 | ) 33 | 34 | # self.problem = QUBO( 35 | # np.array( 36 | # [[-5, 2, 4, 0], [2, -3, 1, 0], [4, 1, -8, 5], [0, 0, 5, -6]] 37 | # ) 38 | # ) 39 | from lava.magma.core.process.variable import Var 40 | 41 | cc = { 42 | 2: Var( 43 | shape=self.problem.cost.coefficients[2].shape, 44 | init=self.problem.cost.coefficients[2], 45 | ), 46 | } 47 | 48 | # Create processes. 49 | self.solution_finder = SolutionFinder( 50 | continuous_var_shape=( 51 | self.problem.variables._continuous.num_variables,), 52 | cost_diagonal=self.problem.cost.coefficients[2].diagonal(), 53 | cost_coefficients=cc, 54 | constraints=None, 55 | hyperparameters={}, 56 | discrete_var_shape=None, 57 | backend="CPU", 58 | problem=self.problem, 59 | ) 60 | 61 | # Execution configurations. 62 | ReadGatePyModel = get_read_gate_model_class(1) 63 | pdict = {ReadGate: ReadGatePyModel} 64 | self.run_cfg = Loihi2SimCfg(exception_proc_model_map=pdict) 65 | self.solution_finder._log_config.level = 20 66 | 67 | def test_create_process(self): 68 | self.assertIsInstance(self.solution_finder, SolutionFinder) 69 | 70 | def test_run(self): 71 | self.solution_finder.run(RunSteps(5), run_cfg=self.run_cfg) 72 | self.solution_finder.stop() 73 | 74 | @unittest.skip("cost_convergence_checker is only available for discrete " 75 | "variable models such as QUBO. And CPU backend of QUBO " 76 | "solver is temporarily deactivated.") 77 | def test_cost_checker_is_connected_to_variables_population(self): 78 | self.solution_finder.run(RunSteps(5), run_cfg=self.run_cfg) 79 | self.solution_finder.stop() 80 | pm = self.solution_finder.model_class(self.solution_finder) 81 | self.assertIs( 82 | pm.cost_convergence_check.cost_components.in_connections[ 83 | 0 84 | ].process, 85 | pm.variables.discrete, 86 | ) 87 | 88 | def test_qp_cost_defines_num_vars_in_discrete_variables_process(self): 89 | self.solution_finder.run(RunSteps(5), run_cfg=self.run_cfg) 90 | self.solution_finder.stop() 91 | pm = self.solution_finder.model_class(self.solution_finder) 92 | self.assertEqual( 93 | pm.variables.continuous.num_variables, 94 | self.problem.variables.continuous.num_variables, 95 | ) 96 | self.assertEqual( 97 | self.solution_finder.variables_assignment.size, 98 | self.problem.variables.continuous.num_variables, 99 | ) 100 | 101 | @unittest.skip("CPU backend of QUBO solver is temporarily deactivated.") 102 | def test_qubo_cost_defines_biases(self): 103 | self.solution_finder.run(RunSteps(5), run_cfg=self.run_cfg) 104 | self.solution_finder.stop() 105 | pm = self.solution_finder.model_class(self.solution_finder) 106 | condition = ( 107 | pm.variables.continuous.cost_diagonal 108 | == self.problem.cost.get_coefficient(2).diagonal() 109 | ).all() 110 | self.assertTrue(condition) 111 | 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/apps/tsp/problems.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import networkx as ntx 6 | import numpy as np 7 | import typing as ty 8 | 9 | 10 | class TravellingSalesmanProblem: 11 | """Travelling Salesman Problem specification. 12 | 13 | N customer nodes need to be visited by a travelling salesman, 14 | while minimizing the overall distance of the traversal. 15 | """ 16 | def __init__(self, 17 | waypt_coords: ty.List[ty.Tuple[int, int]], 18 | starting_pt: ty.Tuple[int, int], 19 | edges: ty.Optional[ty.List[ty.Tuple[int, int]]] = None): 20 | """ 21 | Parameters 22 | ---------- 23 | node_coords : list(tuple(int, int)) 24 | A list of integer tuples corresponding to node coordinates. 25 | Nodes signify the "customers" in a VRP, which need to be visited 26 | by the vehicles. 27 | vehicle_coords : list(tuple(int, int)) 28 | A list of integer tuples corresponding to the initial vehicle 29 | coordinates. If the length of the list is 1, then it is 30 | assumed that all vehicles begin from the same depot. 31 | edges: (Optional) list(tuple(int, int)) 32 | An optional list of edges connecting nodes, given as a list of 33 | node ID pairs. If None provided, assume all-to-all connectivity 34 | between nodes. 35 | 36 | Notes 37 | ----- 38 | The vehicle IDs and node IDs are assigned serially. The IDs 1 to M 39 | correspond to vehicles and (M+1) to (M+N) correspond to nodes to be 40 | visited by the vehicles. 41 | """ 42 | super().__init__() 43 | self._waypt_coords = waypt_coords 44 | self._starting_pt_coords = starting_pt 45 | self._num_waypts = len(self._waypt_coords) 46 | self._starting_pt_id = 1 47 | self._waypt_ids = list(np.arange(2, self._num_waypts + 2)) 48 | self._nodes = {self._starting_pt_id: self._starting_pt_coords} 49 | self._nodes.update(dict(zip(self._waypt_ids, self._waypt_coords))) 50 | if edges: 51 | self._edges = edges 52 | else: 53 | self._edges = [] 54 | 55 | self._problem_graph = None 56 | 57 | @property 58 | def nodes(self): 59 | return self._nodes 60 | 61 | @nodes.setter 62 | def nodes(self, nodes: ty.Dict[int, ty.Tuple[int, int]]): 63 | self._nodes = nodes 64 | 65 | @property 66 | def node_ids(self): 67 | return list(self._nodes.keys()) 68 | 69 | @property 70 | def node_coords(self): 71 | return list(self._nodes.values()) 72 | 73 | @property 74 | def num_nodes(self): 75 | return len(list(self._nodes.keys())) 76 | 77 | @property 78 | def edges(self): 79 | return self._edges 80 | 81 | @property 82 | def waypt_coords(self): 83 | return self._waypt_coords 84 | 85 | @property 86 | def waypt_ids(self): 87 | return self._waypt_ids 88 | 89 | @property 90 | def num_waypts(self): 91 | return len(self._waypt_coords) 92 | 93 | @property 94 | def problem_graph(self): 95 | """NetworkX problem graph is created and returned. 96 | 97 | If edges are specified, they are taken into account. 98 | Returns 99 | ------- 100 | A graph object corresponding to the problem. 101 | """ 102 | if not self._problem_graph: 103 | self._generate_problem_graph() 104 | return self._problem_graph 105 | 106 | def _generate_problem_graph(self): 107 | if len(self.edges) > 0: 108 | gph = ntx.DiGraph() 109 | # Add the nodes to be visited 110 | gph.add_nodes_from(self.node_ids) 111 | # If there are user-provided edges, add them between the nodes 112 | gph.add_edges_from(self.edges) 113 | else: 114 | gph = ntx.complete_graph(self.node_ids, create_using=ntx.DiGraph()) 115 | 116 | ntx.set_node_attributes(gph, self.nodes, name="Coordinates") 117 | 118 | # Compute Euclidean distance along all edges and assign them as edge 119 | # weights 120 | # ToDo: Replace the loop with independent distance matrix computation 121 | # and then assign the distances as attributes 122 | for edge in gph.edges.keys(): 123 | gph.edges[edge]["cost"] = np.linalg.norm( 124 | np.array(gph.nodes[edge[1]]["Coordinates"]) - np.array( 125 | gph.nodes[edge[0]]["Coordinates"])) 126 | 127 | self._problem_graph = gph 128 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/problems/test_variables.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | 6 | import unittest 7 | 8 | from lava.lib.optimization.problems.variables import ( 9 | ContinuousVariables, 10 | DiscreteVariables, 11 | Variable, 12 | Variables, 13 | ) 14 | 15 | 16 | class TestVariable(unittest.TestCase): 17 | def setUp(self) -> None: 18 | self.name = "var" 19 | self.var = Variable(name=self.name) 20 | 21 | def test_obj_creation(self): 22 | self.assertIsInstance(self.var, Variable) 23 | 24 | def test_value_assignment(self): 25 | self.var.value = 10 26 | self.assertEqual(self.var.value, 10) 27 | 28 | def test_initial_value_is_none(self): 29 | self.assertIsNone(self.var.value) 30 | 31 | def test_name_validation(self): 32 | self.assertIs(self.var.name, self.name) 33 | 34 | 35 | class TestDiscreteVariables(unittest.TestCase): 36 | def setUp(self) -> None: 37 | self.domains = [10, 3, 11] 38 | self.dvars = DiscreteVariables(domains=self.domains) 39 | 40 | def test_obj_creation(self): 41 | self.assertIsInstance(self.dvars, DiscreteVariables) 42 | 43 | def test_variable_set_creation(self): 44 | for n, var in enumerate(self.dvars.variable_set): 45 | with self.subTest(msg=f"ID={n}"): 46 | self.assertIsInstance(var, Variable) 47 | 48 | def test_len_variable_set(self): 49 | self.assertEqual(len(self.dvars.variable_set), 3) 50 | 51 | def test_num_variables(self): 52 | self.assertEqual(self.dvars.num_variables, 3) 53 | 54 | def test_domain_sizes(self): 55 | self.assertEqual(self.dvars.domain_sizes, self.domains) 56 | 57 | def test_domains(self): 58 | for n, domain in enumerate(self.dvars.domains): 59 | with self.subTest(msg=f"{n}"): 60 | self.assertEqual(len(domain), self.domains[n]) 61 | 62 | def test_set_domains(self): 63 | self.dvars.domains = [4, 5, 4, 5] 64 | self.assertEqual(self.dvars.domain_sizes, [4, 5, 4, 5]) 65 | 66 | def test_domains_defaults_to_none(self): 67 | dvars = DiscreteVariables() 68 | self.assertIsNone(dvars.domains) 69 | 70 | @unittest.skip("WIP") 71 | def test_domains_validation(self): 72 | pass 73 | 74 | 75 | class TestContinuousVariables(unittest.TestCase): 76 | def setUp(self) -> None: 77 | self.bounds = [(0, 1), (0, 20), (-1, 4)] 78 | self.cvars = ContinuousVariables( 79 | num_variables=len(self.bounds), bounds=self.bounds 80 | ) 81 | 82 | def test_obj_creation(self): 83 | self.assertIsInstance(self.cvars, ContinuousVariables) 84 | 85 | def test_set_bounds(self): 86 | self.cvars.bounds = [(1, 2), (0, 5)] 87 | self.assertEqual(self.cvars.bounds, [(1, 2), (0, 5)]) 88 | 89 | def test_num_variables(self): 90 | self.assertEqual(self.cvars.num_variables, 3) 91 | 92 | def test_variable_set_creation(self): 93 | for n, var in enumerate(self.cvars.variable_set): 94 | with self.subTest(msg=f"ID={n}"): 95 | self.assertIsInstance(var, Variable) 96 | 97 | def test_len_variable_set(self): 98 | self.assertEqual(len(self.cvars.variable_set), 3) 99 | 100 | def test_get_bounds(self): 101 | self.assertIs(self.cvars.bounds, self.bounds) 102 | 103 | @unittest.skip("WIP") 104 | def test_bounds_validation(self): 105 | pass 106 | 107 | 108 | class TestVariables(unittest.TestCase): 109 | def setUp(self) -> None: 110 | self.vars = Variables() 111 | 112 | def test_obj_creation(self): 113 | self.assertIsInstance(self.vars, Variables) 114 | 115 | def test_has_discrete_bucket(self): 116 | self.assertIsInstance(self.vars.discrete, DiscreteVariables) 117 | 118 | def test_has_continuous_bucket(self): 119 | self.assertIsInstance(self.vars.continuous, ContinuousVariables) 120 | 121 | def test_set_discrete(self): 122 | new_vars = DiscreteVariables([3, 3, 3]) 123 | self.vars.discrete = new_vars 124 | self.assertIs(self.vars.discrete, new_vars) 125 | 126 | def test_set_continuous(self): 127 | new_vars = ContinuousVariables([(1, 4), (0, 1)]) 128 | self.vars.continuous = new_vars 129 | self.assertIs(self.vars.continuous, new_vars) 130 | 131 | def test_discrete_attribute_class(self): 132 | self.assertIsInstance(self.vars.discrete, DiscreteVariables) 133 | 134 | def test_continuous_attribute_class(self): 135 | self.assertIsInstance(self.vars.continuous, ContinuousVariables) 136 | 137 | 138 | if __name__ == "__main__": 139 | unittest.main() 140 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/monitoring_processes/solution_readout/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022-2024 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import numpy as np 5 | from lava.lib.optimization.solvers.generic.monitoring_processes\ 6 | .solution_readout.process import SolutionReadout 7 | from lava.magma.core.decorator import implements, requires 8 | from lava.magma.core.model.py.model import PyLoihiProcessModel 9 | from lava.magma.core.model.py.ports import PyInPort 10 | from lava.magma.core.model.py.type import LavaPyType 11 | from lava.magma.core.resources import CPU 12 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 13 | 14 | 15 | @implements(SolutionReadout, protocol=LoihiProtocol) 16 | @requires(CPU) 17 | class SolutionReadoutPyModel(PyLoihiProcessModel): 18 | """CPU model for the SolutionReadout process. 19 | The process receives two types of messages, an updated cost and the 20 | state of 21 | the solver network representing the current candidate solution to an 22 | OptimizationProblem. Additionally, a target cost can be defined by the 23 | user, once this cost is reached by the solver network, this process 24 | will request the runtime service to pause execution. 25 | """ 26 | 27 | solution: np.ndarray = LavaPyType(np.ndarray, np.int32, 32) 28 | solution_step: np.ndarray = LavaPyType(np.ndarray, np.int32, 32) 29 | time_steps_per_algorithmic_step: np.ndarray = LavaPyType(np.ndarray, 30 | np.int32, 32) 31 | read_solution: PyInPort = LavaPyType( 32 | PyInPort.VEC_DENSE, np.int32, precision=32 33 | ) 34 | cost_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.int32, precision=32) 35 | timestep_in: PyInPort = LavaPyType( 36 | PyInPort.VEC_DENSE, np.int32, precision=32 37 | ) 38 | target_cost: int = LavaPyType(int, np.int32, 32) 39 | min_cost: np.ndarray = LavaPyType(np.ndarray, np.int32, 32) 40 | stop = False 41 | 42 | def run_spk(self): 43 | if self.stop: 44 | return 45 | raw_cost, min_cost_id = self.cost_in.recv() 46 | if raw_cost != 0: 47 | timestep, raw_solution = self._receive_data() 48 | cost = self.decode_cost(raw_cost) 49 | self.solution_step = abs(timestep) 50 | self.solution[:] = self.decode_solution( 51 | raw_solution=raw_solution, 52 | time_steps_per_algorithmic_step=self. 53 | time_steps_per_algorithmic_step) 54 | self.min_cost[:] = np.asarray([cost[0], min_cost_id]) 55 | if cost[0] < 0: 56 | self._printout_new_solution(cost, min_cost_id, timestep) 57 | self._printout_if_converged() 58 | self._stop_if_requested(timestep, min_cost_id) 59 | 60 | def _receive_data(self): 61 | timestep = self.timestep_in.recv()[0] 62 | raw_solution = self.read_solution.recv() 63 | return timestep, raw_solution 64 | 65 | @staticmethod 66 | def decode_cost(raw_cost) -> np.ndarray: 67 | return np.array([raw_cost]).astype(np.int32) 68 | 69 | @staticmethod 70 | def decode_solution(raw_solution, 71 | time_steps_per_algorithmic_step) -> np.ndarray: 72 | if time_steps_per_algorithmic_step == 1: 73 | raw_solution &= 0x1F # AND with 0x1F (=0b11111) retains 5 LSBs 74 | # The binary solution was attained 2 steps ago. Shift down by 4. 75 | return raw_solution.astype(np.int8) >> 4 76 | elif time_steps_per_algorithmic_step == 2: 77 | raw_solution &= 0x7 # AND with 0x7 (=0b111) retains 3 LSBs 78 | # The binary solution was attained 1 step ago. Shift down by 4. 79 | return raw_solution.astype(np.int8) >> 2 80 | else: 81 | raise ValueError(f"The number of time steps that a single " 82 | f"algorithmic step requires must be either 1 or " 83 | f"2 but is {time_steps_per_algorithmic_step}.") 84 | 85 | def _printout_new_solution(self, cost, min_cost_id, timestep): 86 | self.log.info( 87 | f"Host: better solution found by network {min_cost_id} at " 88 | f"step {abs(timestep) - 2} " 89 | f"with cost {cost[0]}: {self.solution}" 90 | ) 91 | 92 | def _printout_if_converged(self): 93 | if ( 94 | self.min_cost[0] is not None 95 | and self.min_cost[0] <= self.target_cost 96 | ): 97 | self.log.info( 98 | f"Host: network reached target cost {self.target_cost}.") 99 | 100 | def _stop_if_requested(self, timestep, min_cost_id): 101 | if (timestep > 0 or timestep == -1) and min_cost_id != -1: 102 | self.stop = True 103 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/bayesian/processes.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: LGPL-2.1-or-later 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | 7 | from lava.magma.core.process.ports.ports import InPort, OutPort 8 | from lava.magma.core.process.process import AbstractProcess 9 | from lava.magma.core.process.variable import Var 10 | 11 | 12 | class BayesianOptimizer(AbstractProcess): 13 | """ 14 | An abstract process defining the internal state and input/output 15 | variables required by all Bayesian optimizers 16 | """ 17 | 18 | def __init__(self, acq_func_config: dict, acq_opt_config: dict, 19 | search_space: np.ndarray, est_config: str, ip_gen_config: dict, 20 | num_ips: int, num_objectives: int, seed: int, 21 | **kwargs) -> None: 22 | """ 23 | Initialize the BayesianOptimizer process 24 | 25 | Parameters 26 | ---------- 27 | acq_func_config : dict 28 | A series of key-value pairs specifying the runtime configuration 29 | of the acquisition function 30 | acq_opt_config : dict 31 | A series of key-value pairs specifying the runtime configuration 32 | of the acquisition optimizer 33 | search_space : np.ndarray 34 | A list of parameters that describe the multi-dimensional search 35 | space of the Bayesian optimization process. The length of this 36 | data structure represents the number of parameters in the search 37 | space with object at each index ( dimensions[idx] ) describing 38 | the range or discrete choices for the idx-th parameters. Your 39 | search space should consist of two types of parameters: 40 | 1) continuous: (, , np.inf) 41 | 2) discrete: (, , ) 42 | The following represents the three main types of parameters: 43 | 1) a continuous variable from -1000.0 to 256 44 | 3) a discrete set of categorical variable, the best way to 45 | convert your categorical variables to this format is to 46 | exploit the discretization of enumerations or parse your 47 | list possible selections using array indexing. 48 | search_space: np.ndarray 49 | est_config : dict 50 | A series of key-value pairs specifying the runtime configuration 51 | of the surrogate function estimator 52 | ip_gen_config : dict 53 | A series of key-value pairs specifying the runtime configuration 54 | of the initial point generator 55 | num_ips : int 56 | An integer specifying the number of points to sample from the 57 | parameter search space before using the surrogate gradients to 58 | exploit the search space 59 | num_objectives : int 60 | An integer specifying the number of qualitative attributes used 61 | to measure the black-box function 62 | seed : int 63 | An integer specifying the random state of all random number 64 | generators 65 | """ 66 | super().__init__(**kwargs) 67 | 68 | num_ss_dimensions: int = search_space.shape[0] 69 | 70 | # Input/Output Ports 71 | input_length: int = num_ss_dimensions + num_objectives 72 | output_length: int = num_ss_dimensions 73 | self.results_in = InPort((input_length, 1)) 74 | self.next_point_out = OutPort((output_length, 1)) 75 | 76 | # General Internal State Variables 77 | sorted_keys: list[str] = sorted(acq_func_config.keys()) 78 | sorted_config: list = [acq_func_config[k] for k in sorted_keys] 79 | sorted_config: np.ndarray = np.array(sorted_config) 80 | self.acq_func_config = Var( 81 | shape=sorted_config.shape, 82 | init=sorted_config 83 | ) 84 | 85 | sorted_keys: list[str] = sorted(acq_func_config.keys()) 86 | sorted_config: list = [acq_opt_config[k] for k in sorted_keys] 87 | sorted_config: np.ndarray = np.array(sorted_config) 88 | self.acq_opt_config = Var( 89 | shape=sorted_config.shape, 90 | init=sorted_config 91 | ) 92 | 93 | sorted_keys: list[str] = sorted(est_config.keys()) 94 | sorted_config: list = [est_config[k] for k in sorted_keys] 95 | sorted_config: np.ndarray = np.array(sorted_config) 96 | self.est_config = Var(shape=sorted_config.shape, init=sorted_config) 97 | 98 | sorted_keys: list[str] = sorted(ip_gen_config.keys()) 99 | sorted_config: list = [ip_gen_config[k] for k in sorted_keys] 100 | sorted_config: np.ndarray = np.array(sorted_config) 101 | self.ip_gen_config = Var( 102 | shape=sorted_config.shape, 103 | init=sorted_config 104 | ) 105 | 106 | self.search_space = Var(search_space.shape, init=search_space) 107 | self.num_ips = Var((1,), init=num_ips) 108 | self.num_objectives = Var((1,), init=num_objectives) 109 | self.seed = Var((1,), init=seed) 110 | 111 | self.results_log = Var((1,), init=np.array([[None]])) 112 | self.num_iterations = Var((1,), init=-1) 113 | self.initialized = Var((1,), init=False) 114 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/nebm/test_models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import os 6 | import unittest 7 | import numpy as np 8 | 9 | from lava.lib.optimization.problems.problems import QUBO 10 | from lava.lib.optimization.solvers.generic.solver import OptimizationSolver 11 | from lava.lib.optimization.solvers.generic.solver import SolverConfig 12 | 13 | 14 | def solve_nebm(q, time=100, target=-10, refract=1, counter=0, temp=0): 15 | qubo_problem = QUBO(q=q) 16 | solver = OptimizationSolver(qubo_problem) 17 | config = SolverConfig( 18 | timeout=time, 19 | target_cost=target, 20 | backend="CPU", 21 | probe_cost=True, 22 | probe_state=True, 23 | hyperparameters={ 24 | "refract": refract, 25 | "refract_counter": counter, 26 | "neuron_model": "nebm", 27 | "temperature": temp, 28 | } 29 | ) 30 | return solver.solve(config=config) 31 | 32 | 33 | @unittest.skip("CPU backend of QUBO solver is temporarily deactivated") 34 | class TestPyNEBM(unittest.TestCase): 35 | def test_boltzmann(self): 36 | # temp = 0 and delta_e >= 0, p(switch) = 0 37 | q = np.zeros((10, 10), dtype=int) 38 | report = solve_nebm(q) 39 | self.assertEqual(report.state_timeseries.sum(), 0) 40 | # temp = 0 and delta_e < 0, p(switch) = 1 41 | q[np.diag_indices(10)] = -1 42 | report = solve_nebm(q) 43 | self.assertEqual(report.best_state.sum(), 10) 44 | # With temp > 0 and delta_e = 0, p(switch) = 0.5 45 | report = solve_nebm(q, temp=10, target=-100) 46 | self.assertAlmostEqual(report.state_timeseries.mean(), 0.5, delta=0.1) 47 | # With delta_e != 0, p(switch) increases as temp increases 48 | q[np.diag_indices(10)] = 1 49 | report = solve_nebm(q, temp=1) 50 | prob_switch_at_t1 = report.state_timeseries.mean() 51 | report = solve_nebm(q, temp=10) 52 | prob_switch_at_t10 = report.state_timeseries.mean() 53 | self.assertLess(prob_switch_at_t1, prob_switch_at_t10) 54 | # With temp > 0, p(switch) decreases as delta_e increases 55 | q[np.diag_indices(10)] = np.arange(-5, 5, 1) 56 | report = solve_nebm(q, temp=1, target=-100, time=1000) 57 | prob_on = report.state_timeseries.mean(axis=0) 58 | diff_prob_on = np.diff(prob_on) 59 | self.assertTrue(np.all(diff_prob_on <= 0)) 60 | 61 | def test_refract_oscillatory(self): 62 | # Check that the model oscillates with no tie-breaking 63 | q = -2 * np.eye(10, dtype=int) 64 | q[1:, 1:] += 1 65 | report = solve_nebm(q) 66 | solution = np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 67 | 68 | self.assertEqual(-2 * solution.sum(), report.best_cost) 69 | np.testing.assert_array_equal(report.best_state, solution) 70 | 71 | actual_cost = report.cost_timeseries.flatten()[:10] 72 | expected_cost = np.array([0, 0, 61, 61, -2, -2, 61, 61, -2, -2]) 73 | np.testing.assert_array_equal(actual_cost, expected_cost) 74 | 75 | def test_refract_descend(self): 76 | # Check that the model descends cost with varied refract 77 | q = -2 * np.eye(10, dtype=int) + 1 78 | report = solve_nebm(q, refract=np.array(range(1, 11))) 79 | solution = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) 80 | 81 | self.assertEqual(-1 * solution.sum(), report.best_cost) 82 | np.testing.assert_array_equal(report.best_state, solution) 83 | 84 | actual_cost = report.cost_timeseries.flatten() 85 | actual_cost = actual_cost[:report.best_timestep + 1] 86 | expected_cost = np.array([0, 0, 80, 80, 63, 48, 35, 24, 15, 87 | 8, 3, 0, -1]) 88 | np.testing.assert_array_equal(actual_cost, expected_cost) 89 | 90 | def test_refract_counter_descend(self): 91 | # Check that the model descends cost with varied refract_counter 92 | q = -1 * np.eye(10, dtype=int) 93 | report = solve_nebm(q, counter=np.array(range(10))) 94 | solution = np.ones(10, dtype=int) 95 | 96 | self.assertEqual(-1 * solution.sum(), report.best_cost) 97 | np.testing.assert_array_equal(report.best_state, solution) 98 | 99 | actual_cost = report.cost_timeseries.flatten() 100 | actual_cost = actual_cost[:report.best_timestep + 1] 101 | expected_cost = np.array([0, 0, -1, -2, -3, -4, -5, -6, -7, 102 | -8, -9, -10]) 103 | np.testing.assert_array_equal(actual_cost, expected_cost) 104 | 105 | def test_robust_descend(self): 106 | # Check that the model descends cost with varied refract_counter 107 | np.random.seed(42) 108 | 109 | q = 1 * (np.random.rand(10, 10) < 0.1) 110 | q -= np.tril(q, 0) 111 | q += np.triu(q, 0).T 112 | q *= (1 - np.eye(10, dtype=int)) 113 | q -= 1 * np.eye(10, dtype=int) 114 | 115 | report = solve_nebm(q, time=10, refract=10, 116 | counter=np.array(range(0, 10)), 117 | temp=0, target=-10) 118 | 119 | diff_cost = np.diff(report.cost_timeseries) 120 | self.assertTrue(np.all(diff_cost <= 0)) 121 | 122 | 123 | if __name__ == "__main__": 124 | unittest.main() 125 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/annealing/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import numpy as np 5 | import typing as ty 6 | from numpy import typing as npty 7 | 8 | from lava.magma.core.process.ports.ports import OutPort 9 | from lava.magma.core.process.process import AbstractProcess 10 | from lava.magma.core.process.variable import Var 11 | 12 | 13 | class Annealing(AbstractProcess): 14 | """ 15 | Neuron that updates the temperature for simulated annealing. 16 | 17 | Parameters 18 | ---------- 19 | shape: Tuple 20 | Number of neurons. Default is (1,). 21 | max_temperature: int, ArrayLike 22 | Both maximum and initial temperature of the annealing schedule. 23 | The temperature defines the noise of the system 24 | min_temperature: ArrayLike 25 | Minimum temperature of the annealing schedule. 26 | steps_per_temperature: int, ArrayLike 27 | The number of time steps between two annealing steps. 28 | delta_temperature: ArrayLike 29 | Defines the change in temperature in each annealing step. 30 | If annealing_schedule is 'linear', the temperature is decreased by 31 | temperature -= delta_temperature . 32 | If annealing_schedule is 'geometric', the temperature is changed by 33 | temperature *= delta_temperature * 2^(exp_temperature) . 34 | exp_temperature: ArrayLike 35 | Defines the change in temperature in each annealing step. For 36 | details, refer to 'delta_temperature' 37 | annealing_schedule: str 38 | Defines the annealing schedule. Supported values are 'linear' and 39 | 'geometric'. 40 | """ 41 | 42 | # annealing schedules that are currently supported 43 | supported_anneal_schedules = ['linear', 'geometric'] 44 | 45 | def __init__( 46 | self, 47 | *, 48 | max_temperature: ty.Union[int, npty.NDArray], 49 | min_temperature: ty.Union[int, npty.NDArray], 50 | delta_temperature: ty.Union[int, npty.NDArray], 51 | steps_per_temperature: ty.Union[int, npty.NDArray], 52 | exp_temperature: ty.Union[int, npty.NDArray], 53 | annealing_schedule: str, 54 | shape: ty.Tuple[int, ...] = (1,), 55 | ): 56 | 57 | self._validate_input( 58 | shape=shape, 59 | min_temperature=min_temperature, 60 | max_temperature=max_temperature, 61 | delta_temperature=delta_temperature, 62 | steps_per_temperature=steps_per_temperature, 63 | exp_temperature=exp_temperature, 64 | annealing_schedule=annealing_schedule, 65 | ) 66 | 67 | super().__init__( 68 | shape=shape, 69 | min_temperature=min_temperature, 70 | max_temperature=max_temperature, 71 | delta_temperature=delta_temperature, 72 | steps_per_temperature=steps_per_temperature, 73 | exp_temperature=exp_temperature, 74 | annealing_schedule=annealing_schedule, 75 | ) 76 | 77 | self.delta_temperature_out = OutPort(shape=shape) 78 | 79 | self.temperature = Var(shape=shape, init=np.int_(max_temperature)) 80 | self.steps_per_temperature = Var( 81 | shape=shape, init=np.int_(steps_per_temperature) 82 | ) 83 | self.temperature_counter = Var( 84 | shape=shape, init=np.int_(steps_per_temperature) 85 | ) 86 | 87 | @property 88 | def shape(self) -> ty.Tuple[int, ...]: 89 | return self.proc_params["shape"] 90 | 91 | def _validate_input(self, shape, min_temperature, max_temperature, 92 | delta_temperature, steps_per_temperature, 93 | exp_temperature, annealing_schedule) -> None: 94 | """Validates input to the annealing neuron.""" 95 | 96 | if min_temperature < 0: 97 | raise ValueError("min_temperature must be >= 0.") 98 | if max_temperature > 2**(16) - 1: 99 | raise ValueError("max_temperature must be < 2^16 - 1") 100 | if min_temperature > max_temperature: 101 | raise ValueError("max_temperature must be >= min_temperature.") 102 | if delta_temperature < 0: 103 | raise ValueError("delta_temperature must be >=0.") 104 | if annealing_schedule == 'geometric' and exp_temperature < 0: 105 | raise ValueError("exp_temperature must be >=0.") 106 | if annealing_schedule not in self.supported_anneal_schedules: 107 | raise ValueError(f"At the moment only the annealing schedules " 108 | f"{self.supported_anneal_schedules} are " 109 | f"supported.") 110 | if steps_per_temperature < 0: 111 | raise ValueError(f"steps_per_temperature is " 112 | f"{steps_per_temperature} but must be > 0.") 113 | if annealing_schedule == 'geometric': 114 | geometric_constant = np.right_shift(delta_temperature, 115 | exp_temperature) 116 | if geometric_constant > 1 or geometric_constant < 0: 117 | raise ValueError(f"delta_temperature >> exp_temperature " 118 | f"should be between 0 to 1, but is" 119 | f" {geometric_constant}.") 120 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/problems/bayesian/test_models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: LGPL-2.1-or-later 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | import unittest 7 | 8 | from lava.magma.core.decorator import implements, requires 9 | from lava.magma.core.model.py.ports import PyInPort, PyOutPort 10 | from lava.magma.core.model.py.model import PyLoihiProcessModel 11 | from lava.magma.core.model.py.type import LavaPyType 12 | from lava.magma.core.process.ports.ports import InPort, OutPort 13 | from lava.magma.core.process.process import AbstractProcess 14 | from lava.magma.core.process.variable import Var 15 | from lava.magma.core.resources import CPU 16 | from lava.magma.core.run_conditions import RunSteps 17 | from lava.magma.core.run_configs import Loihi1SimCfg 18 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 19 | 20 | from lava.lib.optimization.problems.bayesian.models import ( 21 | DualInputFunction, 22 | SingleInputFunction 23 | ) 24 | 25 | 26 | class InputParamVecProcess(AbstractProcess): 27 | def __init__(self, num_params: int, spike: np.ndarray, **kwargs) -> None: 28 | """Process to set an input parameter vector to evaluate black-box 29 | function accuracy 30 | 31 | num_params : int 32 | the number of parameters to send to the test function 33 | spike : np.ndarray 34 | the parameter vector to send to the black-box process 35 | """ 36 | super().__init__(**kwargs) 37 | 38 | self.x_out = OutPort(shape=(num_params, 1)) 39 | self.data = Var(shape=(num_params, 1), init=spike) 40 | 41 | 42 | class OutputPerfVecProcess(AbstractProcess): 43 | def __init__(self, num_params: int, num_objectives: int, 44 | **kwargs) -> None: 45 | """Process to validate the resulting performance vector from the 46 | black-box function 47 | 48 | num_params : int 49 | the number of parameters within each performance vector 50 | num_objectives : int 51 | the number of objectives within each performance vector 52 | valid_spike : np.ndarray 53 | the expected performance vector 54 | """ 55 | super().__init__(**kwargs) 56 | 57 | perf_vec_length: int = num_params + num_objectives 58 | self.y_in = InPort(shape=(perf_vec_length, 1)) 59 | self.recv_data = Var(shape=(perf_vec_length, 1)) 60 | 61 | 62 | @implements(proc=InputParamVecProcess, protocol=LoihiProtocol) 63 | @requires(CPU) 64 | class PyInputParamVecModel(PyLoihiProcessModel): 65 | x_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, np.float64) 66 | data: np.ndarray = LavaPyType(np.ndarray, np.float64) 67 | 68 | def run_spk(self) -> None: 69 | """send the test data to the black-box process""" 70 | self.x_out.send(self.data) 71 | 72 | 73 | @implements(proc=OutputPerfVecProcess, protocol=LoihiProtocol) 74 | @requires(CPU) 75 | class PyOutputPerfVecProcess(PyLoihiProcessModel): 76 | y_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.float64) 77 | recv_data: np.ndarray = LavaPyType(np.ndarray, np.float64) 78 | 79 | def run_spk(self) -> None: 80 | """receive the result vector from the black-box process""" 81 | self.recv_data = self.y_in.recv() 82 | 83 | 84 | class TestModels(unittest.TestCase): 85 | """Tests all model behaviors associated with the Bayesian problems 86 | 87 | Refer to Bayesian models.py to learn more about behaviors. 88 | """ 89 | 90 | def test_model_dual_cont_input_func(self) -> None: 91 | """test behavior of the DualInputFunction process""" 92 | 93 | input_spike = np.ndarray((2, 1), buffer=np.array([0.1, 0.1])) 94 | valid_spike = np.array([0.1, 0.1, 1.00540399861]) 95 | 96 | input_probe = InputParamVecProcess(num_params=2, spike=input_spike) 97 | bb_process = DualInputFunction() 98 | output_probe = OutputPerfVecProcess(num_params=2, num_objectives=1) 99 | 100 | input_probe.x_out.connect(bb_process.x_in) 101 | bb_process.y_out.connect(output_probe.y_in) 102 | 103 | output_probe.run( 104 | condition=RunSteps(num_steps=1), 105 | run_cfg=Loihi1SimCfg() 106 | ) 107 | 108 | result: np.ndarray = output_probe.recv_data.get() 109 | self.assertEqual(result[0][0], valid_spike[0]) 110 | self.assertEqual(result[1][0], valid_spike[1]) 111 | self.assertAlmostEqual(result[2][0], valid_spike[2]) 112 | 113 | output_probe.stop() 114 | 115 | def test_model_single_input_nonlinear_func(self) -> None: 116 | """test behavior of the SingleInputFunction process""" 117 | 118 | input_spike = np.array([5]) 119 | valid_spike = np.array([5, 0.727989444555]) 120 | 121 | input_probe = InputParamVecProcess(num_params=1, spike=input_spike) 122 | bb_process = SingleInputFunction() 123 | output_probe = OutputPerfVecProcess(num_params=1, num_objectives=1) 124 | 125 | input_probe.x_out.connect(bb_process.x_in) 126 | bb_process.y_out.connect(output_probe.y_in) 127 | 128 | output_probe.run( 129 | condition=RunSteps(num_steps=1), 130 | run_cfg=Loihi1SimCfg() 131 | ) 132 | 133 | result: np.ndarray = output_probe.recv_data.get() 134 | self.assertEqual(result[0][0], valid_spike[0]) 135 | self.assertAlmostEqual(result[1][0], valid_spike[1]) 136 | 137 | output_probe.stop() 138 | 139 | 140 | if __name__ == "__main__": 141 | unittest.main() 142 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/generic/scif/process.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2021-22 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import typing as ty 6 | from numpy import typing as npty 7 | 8 | import numpy as np 9 | 10 | from lava.magma.core.process.process import AbstractProcess 11 | from lava.magma.core.process.variable import Var 12 | from lava.magma.core.process.ports.ports import InPort, OutPort 13 | 14 | 15 | class AbstractScif(AbstractProcess): 16 | """Abstract Process for Stochastic Constraint Integrate-and-Fire 17 | (SCIF) neurons. 18 | """ 19 | 20 | def __init__( 21 | self, 22 | *, 23 | shape: ty.Tuple[int, ...], 24 | step_size: ty.Optional[ty.Union[int, npty.NDArray]] = 1, 25 | theta: ty.Optional[int] = 4, 26 | sustained_on_tau: ty.Optional[int] = 0, 27 | noise_amplitude: ty.Optional[int] = 0, 28 | noise_precision: ty.Optional[int] = 0, 29 | init_value: ty.Optional[ty.Union[int, npty.NDArray]] = 0, 30 | init_state: ty.Optional[ty.Union[int, npty.NDArray]] = 0, 31 | ) -> None: 32 | """ 33 | Stochastic Constraint Integrate and Fire neuron Process. 34 | 35 | Parameters 36 | ---------- 37 | shape: Tuple 38 | Number of neurons. Default is (1,). 39 | step_size: int 40 | The bias driving a SCIF neuron. Default is arbitrarily chosen to 41 | be 1. In the case of quadratic unconstrained binary optimization 42 | (QUBO) problems, `step_size` is the vector formed by the diagonal 43 | elements of the cost matrix. 44 | theta: int 45 | The threshold above which a SCIF neuron would fire a winner-take-all 46 | spike. Default is arbitrarily chosen to be 4. 47 | sustained_on_tau: int 48 | The time constant for which a SCIF neuron's state is sustained 49 | as "ON". The neuron issues a +1 spike when it crosses `theta`, 50 | signifying the beginning of the "ON" period and issues a -1 spike 51 | at the end of `sustained_on_tau` steps, signifying the end of the 52 | "ON" period. 53 | noise_amplitude: int 54 | The multiplicative amplitude of pseudorandom noise added to SCIF 55 | dynamics. 56 | noise_precision: int 57 | The precision (in bits) of the pseudorandom noise added to SCIF 58 | dynamics. 59 | init_value : int, np.ndarray 60 | The spiking history with which the network is initialized 61 | init_state : int, np.ndarray 62 | The state of neurons with which the network is initialized 63 | """ 64 | super().__init__(shape=shape) 65 | 66 | if sustained_on_tau > 0: 67 | AssertionError( 68 | "Sustained ON period for SCIF neurons cannot " 69 | "be positive (sustained_on_tau should be " 70 | "negative). The Timer 'counts up' from " 71 | "sustained_on_tau to 0." 72 | ) 73 | 74 | self.a_in = InPort(shape=shape) 75 | self.s_sig_out = OutPort(shape=shape) 76 | self.s_wta_out = OutPort(shape=shape) 77 | 78 | self.state = Var(shape=shape, init=init_state) 79 | self.cnstr_intg = Var( 80 | shape=shape, init=np.zeros(shape=shape).astype(int) 81 | ) 82 | self.spk_hist = Var(shape=shape, init=init_value) 83 | self.noise_ampl = Var(shape=shape, init=noise_amplitude) 84 | self.noise_prec = Var(shape=shape, init=noise_precision) 85 | self.step_size = Var(shape=shape, init=step_size) 86 | 87 | self.sustained_on_tau = Var(shape=(1,), init=sustained_on_tau) 88 | self.theta = Var(shape=(1,), init=int(theta)) 89 | 90 | @property 91 | def shape(self) -> ty.Tuple[int, ...]: 92 | return self.proc_params["shape"] 93 | 94 | 95 | class CspScif(AbstractScif): 96 | """Stochastic Constraint Integrate-and-Fire neurons to solve CSPs.""" 97 | 98 | def __init__( 99 | self, 100 | *, 101 | shape: ty.Tuple[int, ...], 102 | step_size: ty.Optional[int] = 1, 103 | theta: ty.Optional[int] = 4, 104 | sustained_on_tau: ty.Optional[int] = -5, 105 | noise_amplitude: ty.Optional[int] = 0, 106 | noise_precision: ty.Optional[int] = 8, 107 | ): 108 | super(CspScif, self).__init__( 109 | shape=shape, 110 | step_size=step_size, 111 | theta=theta, 112 | sustained_on_tau=sustained_on_tau, 113 | noise_amplitude=noise_amplitude, 114 | noise_precision=noise_precision, 115 | ) 116 | 117 | 118 | class QuboScif(AbstractScif): 119 | """Stochastic Constraint Integrate-and-Fire neurons to solve QUBO 120 | problems. 121 | """ 122 | 123 | def __init__( 124 | self, 125 | *, 126 | shape: ty.Tuple[int, ...], 127 | cost_diag: npty.NDArray, 128 | theta: ty.Optional[int] = 4, 129 | sustained_on_tau: ty.Optional[int] = 0, 130 | noise_amplitude: ty.Optional[int] = 0, 131 | noise_precision: ty.Optional[int] = 8, 132 | ): 133 | super(QuboScif, self).__init__( 134 | shape=shape, 135 | step_size=cost_diag, 136 | theta=theta, 137 | sustained_on_tau=sustained_on_tau, 138 | noise_amplitude=noise_amplitude, 139 | noise_precision=noise_precision, 140 | ) 141 | 142 | self.cost_diagonal = Var(shape=shape, init=cost_diag) 143 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [build-system] 3 | requires = ["poetry-core>=1.2.0"] 4 | build-backend = "poetry.core.masonry.api" 5 | 6 | [tool.poetry] 7 | name = "lava-optimization" 8 | packages = [ 9 | {include = "lava", from = "src"}, 10 | {include = "tests"} 11 | ] 12 | include = ["tutorials"] 13 | version = "0.6.0.dev0" 14 | readme = "README.md" 15 | description = "A library of solvers that leverage neuromorphic hardware for constrained optimization. Lava-Optimization is part of Lava Framework. Lava-optimization is part of Lava Framework" 16 | homepage = "https://lava-nc.org/" 17 | repository = "https://github.com/lava-nc/lava-optimization" 18 | authors = [ 19 | "Intel's Neuromorphic Computing Lab and the open source community " 20 | ] 21 | license = "(BSD-3-Clause)" 22 | keywords = [ 23 | "neuromorphic", 24 | "ai", 25 | "artificial intelligence", 26 | "neural models", 27 | "spiking neural networks", 28 | "deep learning", 29 | "optimization" 30 | ] 31 | classifiers = [ 32 | "Intended Audience :: Science/Research", 33 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | "Development Status :: 4 - Beta", 36 | "Programming Language :: Python :: 3 :: Only", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "License :: OSI Approved :: BSD License", 40 | "Operating System :: OS Independent" 41 | ] 42 | 43 | [tool.poetry.urls] 44 | "Issue and Bug Tracker" = "https://github.com/lava-nc/lava-optimization/issues" 45 | "Questions and Answers" = "https://github.com/lava-nc/lava-optimization/discussions/categories/q-a" 46 | "Frequently Asked Questions" = "https://github.com/lava-nc/lava-optimization/wiki/Frequently-Asked-Questions-(FAQ)" 47 | "Discussions" = "https://github.com/lava-nc/lava-optimization/discussions" 48 | 49 | [tool.poetry.dependencies] 50 | python = ">=3.10, <3.11" 51 | 52 | lava-nc = { git = "https://github.com/lava-nc/lava.git", branch = "main", develop = true } 53 | 54 | numpy = "^1.24.4" 55 | networkx = "<=2.8" 56 | dsolve = "^0.0.5" 57 | schema = "^0.7.5" 58 | scikit-optimize = "^0.9.0" 59 | scipy = "^1.10.1" 60 | nbformat = "^5.7.1" 61 | seaborn = "^0.12.2" 62 | 63 | 64 | [tool.poetry.dev-dependencies] 65 | bandit = "1.7.4" 66 | coverage = "^6.3.2" 67 | darglint = "^1.8.1" 68 | flake8 = "^4.0.1" 69 | flake8-bandit = "^3.0.0" 70 | flake8-bugbear = "^22.1.11" 71 | flake8-builtins = "^1.5.3" 72 | flake8-comprehensions = "^3.8.0" 73 | flake8-docstrings = "^1.6.0" 74 | flake8-eradicate = "^1.2.0" 75 | flake8-isort = "^4.1.1" 76 | flake8-mutable = "^1.2.0" 77 | flake8-pytest-style = "^1.6.0" 78 | flake8-spellcheck = "^0.25.0" 79 | flakeheaven = "^3.2.1" 80 | matplotlib = "^3.5.1" 81 | pep8-naming = "^0.11.1" 82 | poetry = "^1.1.13" 83 | pytest = "^7.2.0" 84 | pytest-cov = "^3.0.0" 85 | unittest2 = "^1.1.0" 86 | black = "^24.3.0" 87 | ipykernel = "^6.15.0" 88 | nbformat = "^5.3.0" 89 | nbconvert = ">=7.2.10, <7.3" 90 | 91 | [tool.black] 92 | line-length = 80 93 | target-version = [ 94 | "py38" 95 | ] 96 | include = "\\.pyi?$" 97 | exclude = """ 98 | ( 99 | /( 100 | \\.eggs 101 | | \\.git 102 | | \\.venv 103 | | _build 104 | | build 105 | | dist 106 | )/ 107 | ) 108 | """ 109 | 110 | [tool.pytest.ini_options] 111 | minversion = "7.0" 112 | addopts = "--strict-markers --cov=src/lava" 113 | testpaths = [ 114 | "tests" 115 | ] 116 | 117 | [tool.coverage.run] 118 | relative_files = true 119 | source = [ 120 | "src/lava", 121 | ] 122 | 123 | [tool.coverage.report] 124 | exclude_lines = [ 125 | "pragma: no cover", 126 | "raise NotImplemented()", 127 | "if __name__ == .__main__.:", 128 | "main()", 129 | "parser\\..", 130 | "argparse\\..", 131 | ] 132 | fail_under = 45 133 | show_missing = true 134 | 135 | [tool.flakeheaven] 136 | extended_default_ignore=[] # Fix for bug while using newer flake8 ver. 137 | format = "grouped" 138 | max_line_length = 80 139 | show_source = true 140 | exclude = ["./docs/"] 141 | 142 | [tool.flakeheaven.plugins] 143 | flake8-bandit = ["+*", "-S322", "-B101"] # Enable a plugin, disable specific checks 144 | 145 | # Disable below plugins temporarily until lint fix is pushed 146 | flake8-bugbear = ["-*"] 147 | flake8-builtins = ["-*"] 148 | flake8-comprehensions = ["-*"] 149 | flake8-darglint = ["-*"] 150 | flake8-docstrings = ["-*"] 151 | flake8-eradicate = ["-*"] 152 | flake8-isort = ["-*"] 153 | flake8-mutable = ["-*"] 154 | flake8-pytest-style = ["-*"] 155 | flake8-spellcheck = ["-*"] 156 | mccabe = ["-*"] 157 | pep8-naming = ["-*"] 158 | 159 | # Excluding until lint fix is pushed 160 | # flake8-bugbear = ["+*"] # Enable a plugin 161 | # flake8-builtins = ["+*"] 162 | # flake8-comprehensions = ["+*"] 163 | # flake8-darglint = ["+*"] 164 | # flake8-docstrings = ["+*"] 165 | # flake8-eradicate = ["+*"] 166 | # flake8-isort = ["+*"] 167 | # flake8-mutable = ["+*"] 168 | # flake8-pytest-style = ["+*"] 169 | # flake8-spellcheck = ["-*"] 170 | # mccabe = ["+*"] 171 | # pep8-naming = ["+*"] 172 | # pyflakes = ["+*"] 173 | # pylint = ["+*"] 174 | 175 | pycodestyle = ["+*", "-W503", "-E203"] 176 | pyflakes = ["-*"] # Disable temporarily until lint fix is pushed 177 | pylint = ["-*"] # Disable temporarily until lint fix is pushed 178 | 179 | [tool.flakeheaven.exceptions."tests/"] 180 | pycodestyle = ["-F401"] # Disable a check 181 | flake8-bandit = ["-S101"] # Ignore asserts for tests 182 | pyflakes = ["-*"] # Disable a plugin 183 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/utils/test_solver_tuner.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import unittest 6 | 7 | import numpy as np 8 | 9 | from lava.lib.optimization.problems.problems import QUBO 10 | from lava.lib.optimization.solvers.generic.solver import ( 11 | OptimizationSolver, SolverConfig, SolverReport 12 | ) 13 | from lava.lib.optimization.utils.solver_tuner import SolverTuner 14 | 15 | 16 | def prepare_solver_and_config(): 17 | """Generate an example QUBO workload.""" 18 | q = np.asarray([[-5, 2, 4, 0], 19 | [2, -3, 1, 0], 20 | [4, 1, -8, 5], 21 | [0, 0, 5, -6]]) 22 | qubo_problem = QUBO(q=q) 23 | 24 | solver = OptimizationSolver(qubo_problem) 25 | config = SolverConfig( 26 | timeout=1000, 27 | target_cost=-11, 28 | backend="CPU", 29 | hyperparameters={ 30 | "neuron_model": "sa" 31 | } 32 | ) 33 | 34 | return solver, config 35 | 36 | 37 | class TestSolverTuner(unittest.TestCase): 38 | """Unit tests for `SolverTuner` class.""" 39 | 40 | def setUp(self) -> None: 41 | """Setup test environment for `SolverTuner` class.""" 42 | self.search_space = [(0,), (10,)] 43 | self.params_names = ['temperature'] 44 | self.shuffle = True 45 | self.seed = 41 46 | self.solver_tuner = SolverTuner(search_space=self.search_space, 47 | params_names=self.params_names, 48 | shuffle=self.shuffle, 49 | seed=self.seed) 50 | 51 | def test_create_obj(self): 52 | """Tests correct instantiation of `SolverTuner` object.""" 53 | self.assertIsInstance(self.solver_tuner, SolverTuner) 54 | 55 | def test_search_space_property(self): 56 | """Tests `search_space` property exists and has the correct value.""" 57 | self.assertEqual(len(self.solver_tuner.search_space), 58 | len(self.search_space)) 59 | self.assertTrue(all(x in self.solver_tuner.search_space 60 | for x in self.search_space)) 61 | 62 | def test_params_names_property(self): 63 | """Tests `params_names` property exists and has the correct value.""" 64 | self.assertEqual(len(self.solver_tuner.params_names), 65 | len(self.params_names)) 66 | self.assertTrue(all(x in self.solver_tuner.params_names 67 | for x in self.params_names)) 68 | 69 | def test_shuffle_property(self): 70 | """Tests `shuffle` property exists and has the correct value.""" 71 | self.assertEqual(self.shuffle, self.solver_tuner.shuffle) 72 | 73 | def test_seed_property(self): 74 | """Tests `seed` property exists and has the correct value.""" 75 | self.assertEqual(self.seed, self.solver_tuner.seed) 76 | 77 | def test_results_property(self): 78 | """Tests `results` property exists and returns the correct type.""" 79 | self.assertIsInstance(self.solver_tuner.results, np.ndarray) 80 | 81 | def test_shuffle_setter(self): 82 | """Tests `shuffle` setter sets the correct value.""" 83 | self.solver_tuner.shuffle = not self.shuffle 84 | self.assertEqual(self.solver_tuner.shuffle, not self.shuffle) 85 | 86 | def test_seed_setter(self): 87 | """Tests `seed` setter sets the correct value.""" 88 | self.solver_tuner.seed = self.seed + 1 89 | self.assertEqual(self.solver_tuner.seed, self.seed + 1) 90 | 91 | def test_search_space_setter(self): 92 | """Tests `search_space` setter sets the correct value.""" 93 | self.solver_tuner.search_space = [self.search_space[0]] 94 | self.assertEqual(self.solver_tuner.search_space, [self.search_space[0]]) 95 | 96 | def test_generate_grid_util(self): 97 | """Tests `generate_grid` method produces the correct search space.""" 98 | params_domains = { 99 | 'temperature': (0, 10), 100 | } 101 | gen_search_space, gen_params_names = SolverTuner.generate_grid( 102 | params_domains=params_domains 103 | ) 104 | self.assertEqual(len(gen_search_space), len(self.search_space)) 105 | self.assertTrue(all(x in gen_search_space for x in self.search_space)) 106 | self.assertEqual(len(gen_params_names), len(self.params_names)) 107 | self.assertTrue(all(x in gen_params_names for x in self.params_names)) 108 | 109 | @unittest.skip("CPU backend of QUBO solver temporarily disabled.") 110 | def test_tune_success(self): 111 | """Tests the correct set of hyper-parameters is found, for a known 112 | problem.""" 113 | 114 | solver, config = prepare_solver_and_config() 115 | 116 | def fitness(report: SolverReport) -> float: 117 | if report.best_cost <= config.target_cost: 118 | return -report.best_timestep 119 | else: 120 | return -float("inf") 121 | 122 | fitness_target = -16 123 | 124 | hyperparams, success = self.solver_tuner.tune( 125 | solver=solver, 126 | fitness_fn=fitness, 127 | fitness_target=fitness_target, 128 | config=config 129 | ) 130 | 131 | self.assertIsNotNone(hyperparams) 132 | 133 | correct_best_temperature = 10 134 | 135 | self.assertEqual(hyperparams['temperature'], correct_best_temperature) 136 | self.assertTrue(success) 137 | 138 | 139 | if __name__ == "__main__": 140 | unittest.main() 141 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/apps/tsp/solver.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import enum 6 | from pprint import pprint 7 | 8 | import numpy as np 9 | import networkx as ntx 10 | import typing as ty 11 | from typing import Tuple, Dict, List 12 | import numpy.typing as npty 13 | from dataclasses import dataclass 14 | 15 | from lava.lib.optimization.problems.problems import QUBO 16 | from lava.lib.optimization.solvers.generic.solver import OptimizationSolver, \ 17 | SolverReport 18 | from lava.lib.optimization.apps.tsp.problems import TravellingSalesmanProblem 19 | from lava.lib.optimization.apps.tsp.utils.q_matrix_generator import QMatrixTSP 20 | 21 | from lava.magma.core.resources import ( 22 | CPU, 23 | Loihi2NeuroCore, 24 | NeuroCore, 25 | ) 26 | from lava.lib.optimization.solvers.generic.solver import SolverConfig 27 | 28 | BACKENDS = ty.Union[CPU, Loihi2NeuroCore, NeuroCore, str] 29 | CPUS = [CPU, "CPU"] 30 | NEUROCORES = [Loihi2NeuroCore, NeuroCore, "Loihi2"] 31 | 32 | BACKEND_MSG = f""" was requested as backend. However, 33 | the solver currently supports only Loihi 2 and CPU backends. 34 | These can be specified by calling solve with any of the following: 35 | backend = "CPU" 36 | backend = "Loihi2" 37 | backend = CPU 38 | backend = Loihi2NeuroCore 39 | backend = NeuroCoreS 40 | The explicit resource classes can be imported from 41 | lava.magma.core.resources""" 42 | 43 | 44 | @dataclass 45 | class TSPConfig(SolverConfig): 46 | """Solver configuration for VRP solver. 47 | 48 | Parameters 49 | ---------- 50 | core_solver : CoreSolver 51 | Core algorithm that solves a given VRP. Possible values are 52 | CoreSolver.VRPY_CPU or CoreSolver.LAVA_QUBO. 53 | 54 | Notes 55 | ----- 56 | VRPConfig class inherits from `SolverConfig` class at 57 | `lava.lib.optimization.solvers.generic.solver`. Please refer to the 58 | documentation for `SolverConfig` to know more about other arguments that 59 | can be passed. 60 | """ 61 | 62 | profile_q_mat_gen: bool = False 63 | only_gen_q_mat: bool = False 64 | 65 | 66 | @dataclass 67 | class TSPSolution: 68 | """TSP solution holds two lists: 69 | - `solution_path_ids` holds the ordered list of IDs of the waypoints, 70 | which forms the path obtained from the QUBO solution. It begins and 71 | ends with `1`, the ID of the salesman node. 72 | - `solution_path_coords` holds the ordered list of tuples, which are 73 | the coordinates of the waypoints. 74 | """ 75 | solution_path_ids: list = None 76 | solution_path_coords: list = None 77 | 78 | 79 | class TSPSolver: 80 | """Solver for vehicle routing problems. 81 | """ 82 | def __init__(self, tsp: TravellingSalesmanProblem): 83 | self.problem = tsp 84 | self._solver = None 85 | self._profiler = None 86 | self.q_gen_time = 0. 87 | self.raw_solution = None 88 | self.solution = TSPSolution() 89 | 90 | @property 91 | def solver(self): 92 | return self._solver 93 | 94 | @property 95 | def profiler(self): 96 | return self._profiler 97 | 98 | def solve(self, scfg: TSPConfig = TSPConfig()): 99 | """ 100 | Solve a TSP using a given solver configuration. 101 | 102 | Parameters 103 | ---------- 104 | scfg (TSPConfig) : Configuration parameters. 105 | 106 | """ 107 | q_matsize = self.problem.num_waypts ** 2 108 | q_mat_obj = QMatrixTSP( 109 | self.problem.waypt_coords, 110 | lamda_dist=1, 111 | lamda_cnstrt=100, 112 | fixed_pt=False, 113 | fixed_pt_range=(-128, 127), 114 | profile_mat_gen=scfg.profile_q_mat_gen 115 | ) 116 | q_mat = q_mat_obj.matrix.astype(int) 117 | if scfg.profile_q_mat_gen: 118 | self.q_gen_time = q_mat_obj.time_to_gen_mat 119 | if not scfg.only_gen_q_mat: 120 | tsp = QUBO(q=q_mat) 121 | tsp_solver = OptimizationSolver(problem=tsp) 122 | hparams = { 123 | 'neuron_model': 'nebm-sa-refract', 124 | 'refract': 50, 125 | 'refract_scaling': 6, 126 | 'init_state': np.random.randint(0, 2, size=(q_matsize,)), 127 | 'min_temperature': 1, 128 | 'max_temperature': 5, 129 | 'steps_per_temperature': 200 130 | } 131 | if not scfg.hyperparameters: 132 | scfg.hyperparameters.update(hparams) 133 | report: SolverReport = tsp_solver.solve(config=scfg) 134 | if report.profiler: 135 | self._profiler = report.profiler 136 | pprint(f"TSP execution took" 137 | f" {np.sum(report.profiler.execution_time)}s") 138 | self.raw_solution: npty.NDArray = \ 139 | report.best_state.reshape((self.problem.num_waypts, 140 | self.problem.num_waypts)).T 141 | else: 142 | self.raw_solution = -1 * np.ones((self.problem.num_waypts, 143 | self.problem.num_waypts)).T 144 | 145 | self.post_process_sol() 146 | 147 | def post_process_sol(self): 148 | ordered_indices = np.nonzero(self.raw_solution) 149 | ordered_indices = list(zip(ordered_indices[0].tolist(), 150 | ordered_indices[1].tolist())) 151 | ordered_indices.sort(key=lambda x: x[1]) 152 | 153 | self.solution.solution_path_ids = [ 154 | self.problem.waypt_ids[node_id[0]] for node_id in ordered_indices] 155 | self.solution.solution_path_coords = [ 156 | self.problem.waypt_coords[node_id[0]] for node_id in 157 | ordered_indices] 158 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/utils/generators/test_clustering_tsp_vrp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import unittest 6 | import numpy as np 7 | 8 | from lava.lib.optimization.utils.generators.clustering_tsp_vrp import ( 9 | AbstractProblem, AbstractClusteringProblem, AbstractTSP, AbstractVRP, 10 | AbstractUniformProblem, AbstractGaussianProblem, 11 | UniformlySampledClusteringProblem, GaussianSampledClusteringProblem) 12 | 13 | 14 | class TestAbstractProblem(unittest.TestCase): 15 | 16 | def test_abstract_problem_init(self): 17 | ap = AbstractProblem() 18 | self.assertIsInstance(ap, AbstractProblem) 19 | 20 | def test_abstract_problem_properties(self): 21 | dom = np.array([[-5, -5], [5, 5]]) 22 | ap = AbstractProblem(num_anchors=5, num_nodes=20, domain=dom) 23 | self.assertEqual(ap.num_anchors, 5) 24 | self.assertEqual(ap.num_nodes, 20) 25 | self.assertEqual(ap.num_pt_per_clust, 4) # 20 // 5 = 4 26 | self.assertTrue(np.all(ap.domain == dom)) 27 | self.assertListEqual(ap.domain_ll.tolist(), [-5, -5]) 28 | self.assertListEqual(ap.domain_ur.tolist(), [5, 5]) 29 | self.assertIsNone(ap.anchor_coords) 30 | self.assertIsNone(ap.node_coords) 31 | 32 | def test_abstract_problem_setters(self): 33 | ap = AbstractProblem() 34 | ap.num_anchors = 4 35 | ap.num_nodes = 36 36 | ap.domain = [(0, 0), (20, 20)] 37 | self.assertEqual(ap.num_anchors, 4) 38 | self.assertEqual(ap.num_nodes, 36) 39 | self.assertTrue(np.all(ap.domain == np.array([(0, 0), (20, 20)]))) 40 | 41 | 42 | class TestAbstractDerivedProblems(unittest.TestCase): 43 | 44 | def test_abstract_clustering_problem(self): 45 | acp = AbstractClusteringProblem(num_clusters=5, 46 | num_points=20) 47 | self.assertEqual(acp.num_clusters, 5) 48 | self.assertEqual(acp.num_anchors, 5) 49 | self.assertEqual(acp.num_points, 20) 50 | self.assertEqual(acp.num_nodes, 20) 51 | self.assertIsNone(acp.center_coords) 52 | self.assertIsNone(acp.point_coords) 53 | 54 | def test_abstract_tsp(self): 55 | atsp = AbstractTSP(num_starting_pts=5, 56 | num_dest_nodes=20) 57 | self.assertEqual(atsp.num_starting_pts, 5) 58 | self.assertEqual(atsp.num_anchors, 5) 59 | self.assertEqual(atsp.num_dest_nodes, 20) 60 | self.assertEqual(atsp.num_nodes, 20) 61 | self.assertIsNone(atsp.starting_coords) 62 | self.assertIsNone(atsp.dest_coords) 63 | 64 | def test_abstract_vrp(self): 65 | avrp = AbstractVRP(num_vehicles=5, 66 | num_waypoints=20) 67 | self.assertEqual(avrp.num_vehicles, 5) 68 | self.assertEqual(avrp.num_anchors, 5) 69 | self.assertEqual(avrp.num_waypoints, 20) 70 | self.assertEqual(avrp.num_nodes, 20) 71 | self.assertIsNone(avrp.vehicle_coords) 72 | self.assertIsNone(avrp.waypoint_coords) 73 | 74 | def test_abstract_uniform_problem(self): 75 | np.random.seed(2) 76 | aup = AbstractUniformProblem(num_anchors=4, 77 | num_nodes=10, 78 | domain=[(-5, -5), (5, 5)]) 79 | gt_anchor_coords = np.array([[3, 3], [1, -3], [3, 2], [-3, -4]]) 80 | gt_node_coords = np.array([[0, -1], [-1, 0], [2, -2], [1, -1], 81 | [-2, 2], [1, -4], [-2, 0], [3, -1], 82 | [1, -2], [4, -3]]) 83 | self.assertTrue(np.all(aup.anchor_coords == gt_anchor_coords)) 84 | self.assertTrue(np.all(aup.node_coords == gt_node_coords)) 85 | 86 | def test_abstract_gaussian_problem(self): 87 | np.random.seed(2) 88 | agp = AbstractGaussianProblem(num_anchors=4, 89 | num_nodes=7, 90 | domain=[(-3, -3), (3, 3)]) 91 | gt_anchor_coords = np.array([[-3, 2], [-3, 0], [-1, 0], [-3, -1]]) 92 | gt_node_coords = np.array([[-4, 2], [-2, -1], [-4, 0], [-2, 0], 93 | [-2, 0], [-1, -1], [0, 0]]) 94 | self.assertTrue(np.all(agp.anchor_coords == gt_anchor_coords)) 95 | self.assertTrue(np.all(agp.node_coords == gt_node_coords)) 96 | 97 | 98 | class TestClusteringProblems(unittest.TestCase): 99 | 100 | def test_uniform_clustering(self): 101 | np.random.seed(2) 102 | ucp = UniformlySampledClusteringProblem(num_clusters=4, 103 | num_points=10, 104 | domain=[(0, 0), (25, 25)]) 105 | gt_center_coords = np.array([[8, 15], [13, 8], [22, 11], [18, 11]]) 106 | gt_point_coords = np.array([[8, 7], [2, 17], [11, 21], [15, 20], 107 | [20, 5], [7, 3], [6, 4], [10, 11], 108 | [19, 7], [6, 10]]) 109 | self.assertTrue(np.all(ucp.center_coords == gt_center_coords)) 110 | self.assertTrue(np.all(ucp.point_coords == gt_point_coords)) 111 | 112 | def test_gaussian_clustering(self): 113 | np.random.seed(2) 114 | gcp = GaussianSampledClusteringProblem(num_clusters=4, 115 | num_points=10, 116 | domain=[(0, 0), (25, 25)], 117 | variance=3) 118 | gt_center_coords = np.array([[8, 15], [13, 8], [22, 11], [18, 11]]) 119 | gt_point_coords = np.array([[4, 13], [3, 17], [10, 10], [9, 8], 120 | [8, 8], [23, 12], [25, 10], [18, 8], 121 | [17, 8], [13, 12]]) 122 | self.assertTrue(np.all(gcp.center_coords == gt_center_coords)) 123 | self.assertTrue(np.all(gcp.point_coords == gt_point_coords)) 124 | 125 | 126 | if __name__ == '__main__': 127 | unittest.main() 128 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/apps/clustering/problems.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2023 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | 5 | import networkx as ntx 6 | import numpy as np 7 | import typing as ty 8 | 9 | 10 | class ClusteringProblem: 11 | """Problem specification for a clustering problem. 12 | 13 | N points need to be clustered into M clusters. 14 | 15 | The cluster centers are *given*. Clustering is done to assign cluster IDs 16 | to points based on the closest cluster centers. 17 | """ 18 | def __init__(self, 19 | point_coords: ty.List[ty.Tuple[int, int]], 20 | center_coords: ty.Union[int, ty.List[ty.Tuple[int, int]]], 21 | edges: ty.Optional[ty.List[ty.Tuple[int, int]]] = None): 22 | """ 23 | Parameters 24 | ---------- 25 | point_coords : list(tuple(int, int)) 26 | A list of integer tuples corresponding to the coordinates of 27 | points to be clustered. 28 | center_coords : list(tuple(int, int)) 29 | A list of integer tuples corresponding to the coordinates of 30 | cluster-centers. 31 | edges : (Optional) list(tuple(int, int, float)) 32 | An optional list of edges connecting points and cluster centers, 33 | given as a list of triples (ID1, ID2, weight). See the note 34 | below for ID-scheme. If None, assume all-to-all connectivity 35 | between points, weighted by their pairwise distances. 36 | 37 | Notes 38 | ----- 39 | IDs 1 to M correspond to cluster centers and (M+1) to (M+N) correspond 40 | to the points to be clustered. 41 | """ 42 | super().__init__() 43 | self._point_coords = point_coords 44 | self._center_coords = center_coords 45 | self._num_points = len(self._point_coords) 46 | self._num_clusters = len(self._center_coords) 47 | self._cluster_ids = list(np.arange(1, self._num_clusters + 1)) 48 | self._point_ids = list(np.arange( 49 | self._num_clusters + 1, self._num_clusters + self._num_points + 1)) 50 | self._points = dict(zip(self._point_ids, self._point_coords)) 51 | self._cluster_centers = dict(zip(self._cluster_ids, 52 | self._center_coords)) 53 | if edges: 54 | self._edges = edges 55 | else: 56 | self._edges = [] 57 | 58 | self._problem_graph = None 59 | 60 | @property 61 | def points(self): 62 | return self._points 63 | 64 | @points.setter 65 | def points(self, points: ty.Dict[int, ty.Tuple[int, int]]): 66 | self._points = points 67 | 68 | @property 69 | def point_ids(self): 70 | return self._point_ids 71 | 72 | @property 73 | def point_coords(self): 74 | return self._point_coords 75 | 76 | @property 77 | def num_points(self): 78 | return self._num_points 79 | 80 | @property 81 | def edges(self): 82 | return self._edges 83 | 84 | @property 85 | def cluster_centers(self): 86 | return self._cluster_centers 87 | 88 | @cluster_centers.setter 89 | def cluster_centers(self, cluster_centers: ty.Dict[int, ty.Tuple[int, 90 | int]]): 91 | self._cluster_centers = cluster_centers 92 | 93 | @property 94 | def cluster_ids(self): 95 | return self._cluster_ids 96 | 97 | @property 98 | def center_coords(self): 99 | return self._center_coords 100 | 101 | @property 102 | def num_clusters(self): 103 | return self._num_clusters 104 | 105 | @property 106 | def problem_graph(self): 107 | """NetworkX problem graph is created and returned. 108 | 109 | If edges are specified, they are taken into account. 110 | Returns 111 | ------- 112 | A graph object corresponding to the problem. 113 | """ 114 | if not self._problem_graph: 115 | self._generate_problem_graph() 116 | return self._problem_graph 117 | 118 | def _generate_problem_graph(self): 119 | if len(self.edges) > 0: 120 | gph = ntx.DiGraph() 121 | # Add the nodes to be visited 122 | gph.add_nodes_from(self.point_ids) 123 | # If there are user-provided edges, add them between the nodes 124 | gph.add_edges_from(self.edges) 125 | else: 126 | gph = ntx.complete_graph(self.point_ids, create_using=ntx.DiGraph()) 127 | 128 | node_type_dict = dict(zip(self.point_ids, 129 | ["Point"] * len(self.point_ids))) 130 | # Associate node type as "Node" and node coordinates as attributes 131 | ntx.set_node_attributes(gph, node_type_dict, name="Type") 132 | ntx.set_node_attributes(gph, self.points, name="Coordinates") 133 | 134 | # Add vehicles as nodes 135 | gph.add_nodes_from(self.cluster_ids) 136 | # Associate node type as "Vehicle" and vehicle coordinates as attributes 137 | cluster_center_type_dict = dict(zip(self.cluster_ids, 138 | ["Cluster Center"] * len( 139 | self.cluster_ids))) 140 | ntx.set_node_attributes(gph, cluster_center_type_dict, name="Type") 141 | ntx.set_node_attributes(gph, self.cluster_centers, name="Coordinates") 142 | 143 | # Add edges from initial vehicle positions to all nodes (oneway edges) 144 | for cid in self.cluster_ids: 145 | for pid in self.points: 146 | gph.add_edge(cid, pid) 147 | 148 | # Compute Euclidean distance along all edges and assign them as edge 149 | # weights 150 | # ToDo: Replace the loop with independent distance matrix computation 151 | # and then assign the distances as attributes 152 | for edge in gph.edges.keys(): 153 | gph.edges[edge]["cost"] = np.linalg.norm( 154 | np.array(gph.nodes[edge[1]]["Coordinates"]) - np.array( 155 | gph.nodes[edge[0]]["Coordinates"])) 156 | 157 | self._problem_graph = gph 158 | -------------------------------------------------------------------------------- /tests/lava/lib/optimization/solvers/generic/test_solver_cpu_backend_parallel.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | # See: https://spdx.org/licenses/ 4 | import unittest 5 | 6 | import numpy as np 7 | from lava.lib.optimization.problems.problems import QUBO 8 | from lava.lib.optimization.solvers.generic.solver import ( 9 | OptimizationSolver, 10 | SolverConfig, 11 | ) 12 | 13 | 14 | @unittest.skip("CPU backend of QUBO solver temporarily disabled.") 15 | class TestParallelOptimizationSolver(unittest.TestCase): 16 | def test_parallel_run(self): 17 | q = np.array( 18 | [[-5, 2, 4, 0], [2, -3, 1, 0], [4, 1, -8, 5], [0, 0, 5, -6]] 19 | ) 20 | problem = QUBO(q) 21 | solution = np.asarray([1, 0, 0, 1]).astype(int) 22 | solver = OptimizationSolver(problem=problem) 23 | solution_cost = solution @ q @ solution 24 | 25 | np.random.seed(2) 26 | 27 | config = SolverConfig( 28 | timeout=50, 29 | target_cost=-11, 30 | backend="CPU", 31 | hyperparameters=[ 32 | { 33 | "neuron_model": "scif", 34 | "noise_amplitude": i, 35 | "noise_precision": 5, 36 | "sustained_on_tau": -3, 37 | } 38 | for i in range(1, 6) 39 | ] 40 | + [{"neuron_model": "nebm"}], 41 | ) 42 | report = solver.solve(config=config) 43 | self.assertEqual(report.best_cost, solution_cost) 44 | 45 | 46 | def solve_workload( 47 | q, reference_solution, noise_precision=5, noise_amplitude=1, on_tau=-3 48 | ): 49 | expected_cost = reference_solution @ q @ reference_solution 50 | problem = QUBO(q) 51 | np.random.seed(2) 52 | solver = OptimizationSolver(problem) 53 | report = solver.solve( 54 | config=SolverConfig( 55 | timeout=20000, 56 | target_cost=expected_cost, 57 | hyperparameters=[ 58 | { 59 | "neuron_model": "scif", 60 | "noise_amplitude": noise_amplitude, 61 | "noise_precision": noise_precision, 62 | "sustained_on_tau": -on_tau_i, 63 | } 64 | for on_tau_i in range(1, 8) 65 | ], 66 | ) 67 | ) 68 | cost = report.best_state @ q @ report.best_state 69 | return report.best_state, cost, expected_cost 70 | 71 | 72 | @unittest.skip("CPU backend of QUBO solver temporarily disabled.") 73 | class TestWorkloads(unittest.TestCase): 74 | def test_solve_polynomial_minimization(self): 75 | """Polynomial minimization with y=-5x_1 -3x_2 -8x_3 -6x_4 + 76 | 4x_1x_2+8x_1x_3+2x_2x_3+10x_3x_4 77 | """ 78 | q = np.array( 79 | [[-5, 2, 4, 0], [2, -3, 1, 0], [4, 1, -8, 5], [0, 0, 5, -6]] 80 | ) 81 | reference_solution = np.asarray([1, 0, 0, 1]).astype(int) 82 | solution, cost, expected_cost = solve_workload( 83 | q, reference_solution, noise_precision=5 84 | ) 85 | self.assertEqual(cost, expected_cost) 86 | 87 | def test_solve_set_packing(self): 88 | q = -np.array( 89 | [[1, -3, -3, -3], [-3, 1, 0, 0], [-3, 0, 1, -3], [-3, 0, -3, 1]] 90 | ) 91 | 92 | reference_solution = np.zeros(4) 93 | np.put(reference_solution, [1, 2], 1) 94 | solution, cost, expected_cost = solve_workload( 95 | q, reference_solution, noise_precision=5 96 | ) 97 | self.assertEqual(cost, expected_cost) 98 | 99 | def test_solve_max_cut_problem(self): 100 | """Max-Cut Problem""" 101 | q = -np.array( 102 | [ 103 | [2, -1, -1, 0, 0], 104 | [-1, 2, 0, -1, 0], 105 | [-1, 0, 3, -1, -1], 106 | [0, -1, -1, 3, -1], 107 | [0, 0, -1, -1, 2], 108 | ] 109 | ) 110 | reference_solution = np.zeros(5) 111 | np.put(reference_solution, [1, 2], 1) 112 | solution, cost, expected_cost = solve_workload( 113 | q, reference_solution, noise_precision=5 114 | ) 115 | self.assertEqual(cost, expected_cost) 116 | 117 | def test_solve_set_partitioning(self): 118 | q = np.array( 119 | [ 120 | [-17, 10, 10, 10, 0, 20], 121 | [10, -18, 10, 10, 10, 20], 122 | [10, 10, -29, 10, 20, 20], 123 | [10, 10, 10, -19, 10, 10], 124 | [0, 10, 20, 10, -17, 10], 125 | [20, 20, 20, 10, 10, -28], 126 | ] 127 | ) 128 | reference_solution = np.zeros(6) 129 | np.put(reference_solution, [0, 4], 1) 130 | solution, cost, expected_cost = solve_workload( 131 | q, reference_solution, noise_precision=6, noise_amplitude=1 132 | ) 133 | self.assertEqual(cost, expected_cost) 134 | 135 | def test_solve_map_coloring(self): 136 | q = np.array( 137 | [ 138 | [-4, 4, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0], 139 | [4, -4, 4, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0], 140 | [4, 4, -4, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2], 141 | [2, 0, 0, -4, 4, 4, 2, 0, 0, 2, 0, 0, 2, 0, 0], 142 | [0, 2, 0, 4, -4, 4, 0, 2, 0, 0, 2, 0, 0, 2, 0], 143 | [0, 0, 2, 4, 4, -4, 0, 0, 2, 0, 0, 2, 0, 0, 2], 144 | [0, 0, 0, 2, 0, 0, -4, 4, 4, 2, 0, 0, 0, 0, 0], 145 | [0, 0, 0, 0, 2, 0, 4, -4, 4, 0, 2, 0, 0, 0, 0], 146 | [0, 0, 0, 0, 0, 2, 4, 4, -4, 0, 0, 2, 0, 0, 0], 147 | [0, 0, 0, 2, 0, 0, 2, 0, 0, -4, 4, 4, 2, 0, 0], 148 | [0, 0, 0, 0, 2, 0, 0, 2, 0, 4, -4, 4, 0, 2, 0], 149 | [0, 0, 0, 0, 0, 2, 0, 0, 2, 4, 4, -4, 0, 0, 2], 150 | [2, 0, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, -4, 4, 4], 151 | [0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 2, 0, 4, -4, 4], 152 | [0, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 2, 4, 4, -4], 153 | ] 154 | ) 155 | reference_solution = np.zeros(15) 156 | np.put(reference_solution, [1, 3, 8, 10, 14], 1) 157 | solution, cost, expected_cost = solve_workload( 158 | q, 159 | reference_solution, 160 | noise_precision=5, 161 | noise_amplitude=1, 162 | on_tau=-1, 163 | ) 164 | self.assertEqual(cost, expected_cost) 165 | 166 | 167 | if __name__ == "__main__": 168 | unittest.main() 169 | -------------------------------------------------------------------------------- /src/lava/lib/optimization/solvers/bayesian/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2022 Intel Corporation 2 | # SPDX-License-Identifier: LGPL-2.1-or-later 3 | # See: https://spdx.org/licenses/ 4 | 5 | import numpy as np 6 | from scipy.optimize import OptimizeResult 7 | from skopt import Optimizer, Space 8 | from skopt.space import Categorical, Integer, Real 9 | from typing import Union 10 | 11 | from lava.magma.core.decorator import implements, requires, tag 12 | from lava.magma.core.model.py.model import PyLoihiProcessModel 13 | from lava.magma.core.model.py.ports import PyInPort, PyOutPort 14 | from lava.magma.core.model.py.type import LavaPyType 15 | from lava.magma.core.resources import CPU 16 | from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol 17 | 18 | from lava.lib.optimization.solvers.bayesian.processes import ( 19 | BayesianOptimizer 20 | ) 21 | 22 | 23 | @implements(proc=BayesianOptimizer, protocol=LoihiProtocol) 24 | @requires(CPU) 25 | @tag('floating_pt') 26 | class PyBayesianOptimizerModel(PyLoihiProcessModel): 27 | """ 28 | A Python-based implementation of the Bayesian Optimizer processes. For 29 | more information, please refer to bayesian/processes.py. 30 | """ 31 | results_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.float64) 32 | next_point_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, np.float64) 33 | 34 | acq_func_config = LavaPyType(np.ndarray, np.ndarray) 35 | acq_opt_config = LavaPyType(np.ndarray, np.ndarray) 36 | search_space = LavaPyType(np.ndarray, np.ndarray) 37 | est_config = LavaPyType(np.ndarray, np.ndarray) 38 | ip_gen_config = LavaPyType(np.ndarray, np.ndarray) 39 | num_ips = LavaPyType(int, int) 40 | num_objectives = LavaPyType(int, int) 41 | seed = LavaPyType(int, int) 42 | 43 | initialized = LavaPyType(bool, bool) 44 | num_iterations = LavaPyType(int, int) 45 | results_log = LavaPyType(np.ndarray, np.ndarray) 46 | 47 | def run_spk(self) -> None: 48 | """tick the model forward by one time-step""" 49 | 50 | if self.initialized: 51 | # receive a result vector from the black-box function 52 | result_vec: np.ndarray = self.results_in.recv() 53 | 54 | opt_result: OptimizeResult = self.process_result_vector( 55 | result_vec 56 | ) 57 | self.results_log[0].append(opt_result) 58 | else: 59 | # initialize the search space from the standard Bayesian 60 | # optimization search space schema; for more information, 61 | # please refer to the init_search_space method 62 | self.search_space = self.init_search_space() 63 | self.optimizer = Optimizer( 64 | dimensions=self.search_space, 65 | base_estimator=self.est_config[0], 66 | n_initial_points=self.num_ips, 67 | initial_point_generator=self.ip_gen_config[0], 68 | acq_func=self.acq_func_config[0], 69 | acq_optimizer=self.acq_opt_config[0], 70 | random_state=self.seed 71 | ) 72 | self.results_log[0]: list[OptimizeResult] = [] 73 | self.initialized: bool = True 74 | self.num_iterations: int = -1 75 | 76 | next_point: list = self.optimizer.ask() 77 | next_point: np.ndarray = np.ndarray( 78 | shape=(len(self.search_space), 1), 79 | buffer=np.array(next_point) 80 | ) 81 | 82 | self.next_point_out.send(next_point) 83 | self.num_iterations += 1 84 | 85 | def __del__(self) -> None: 86 | """finalize the optimization processing upon runtime conclusion""" 87 | 88 | if hasattr(self, "results_log") and len(self.results_log) > 0: 89 | print(self.results_log[-1]) 90 | 91 | def init_search_space(self) -> list: 92 | """initialize the search space from the standard schema 93 | 94 | This method is designed to convert the numpy ndarray-based search 95 | space description int scikit-optimize format compatible with all 96 | lower-level processes. Your search space should consist of three 97 | types of parameters: 98 | 99 | 1) ("continuous", , , np.nan, ) 100 | 2) ("integer", , , np.nan, ) 101 | 3) ("categorical", np.nan, np.nan, , ) 102 | 103 | Returns 104 | ------- 105 | search_space : list[Union[Real, Integer]] 106 | A collection of continuous and discrete dimensions that represent 107 | the entirety of the problem search space 108 | """ 109 | search_space: list[Union[Real, Integer]] = [] 110 | 111 | for i in range(self.search_space.shape[0]): 112 | p_type: str = self.search_space[i, 0] 113 | minimum: Union[int, float] = self.search_space[i, 1] 114 | maximum: Union[int, float] = self.search_space[i, 2] 115 | choices: list = self.search_space[i, 3] 116 | name: str = self.search_space[i, 4] 117 | 118 | factory_function: dict = { 119 | "continuous": (lambda: Real(minimum, maximum, name=name)), 120 | "integer": (lambda: Integer(minimum, maximum, name=name)), 121 | "categorical": (lambda: Categorical(choices, name=name)) 122 | } 123 | 124 | if p_type not in factory_function.keys(): 125 | raise ValueError( 126 | f"parameter type [{p_type}] is not in valid " 127 | + f"parameter types: {factory_function.keys()}" 128 | ) 129 | 130 | dimension_lambda = factory_function[p_type] 131 | dimension = dimension_lambda() 132 | search_space.append(dimension) 133 | 134 | if not len(search_space) > 0: 135 | raise ValueError("search space is empty") 136 | 137 | return search_space 138 | 139 | def process_result_vector(self, vec: np.ndarray) -> None: 140 | """parse vec into params/objectives before informing optimizer 141 | 142 | Parameters 143 | ---------- 144 | vec : np.ndarray 145 | A single array of data from the black-box process containing 146 | all parameters and objectives for a total length of num_params 147 | + num_objectives 148 | """ 149 | vec: list = vec[:, 0].tolist() 150 | 151 | evaluated_point: list = vec[:-self.num_objectives] 152 | performance: list = vec[-self.num_objectives:] 153 | 154 | return self.optimizer.tell(evaluated_point, performance[0]) 155 | --------------------------------------------------------------------------------