├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.rst ├── codableopt ├── __init__.py ├── interface │ ├── __init__.py │ ├── interface.py │ ├── system_args_map.py │ ├── system_constraint.py │ ├── system_objective.py │ ├── system_problem.py │ └── system_variable.py ├── package_info.py └── solver │ ├── __init__.py │ ├── formulation │ ├── __init__.py │ ├── args_map │ │ ├── __init__.py │ │ └── solver_args_map.py │ ├── constraint │ │ ├── __init__.py │ │ ├── solver_constraints.py │ │ ├── solver_liner_constraints.py │ │ └── solver_user_define_constraint.py │ ├── objective │ │ ├── __init__.py │ │ └── solver_objective.py │ ├── solver_problem.py │ └── variable │ │ ├── __init__.py │ │ ├── solver_category_variable.py │ │ ├── solver_double_variable.py │ │ ├── solver_integer_variable.py │ │ ├── solver_variable.py │ │ └── solver_variable_factory.py │ ├── opt_solver.py │ ├── optimizer │ ├── __init__.py │ ├── entity │ │ ├── __init__.py │ │ ├── proposal_to_move.py │ │ └── score_info.py │ ├── method │ │ ├── __init__.py │ │ ├── optimizer_method.py │ │ └── penalty_adjustment_method.py │ ├── optimization_solver.py │ ├── optimization_state.py │ └── optimizer.py │ └── sampler │ ├── __init__.py │ └── var_value_array_sampler.py ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── manual │ ├── advanced_usage.rst │ ├── algorithm.rst │ └── getting_started.rst ├── reference │ └── class_reference.rst └── sample │ └── sample_codes.rst ├── pyproject.toml ├── requirements_doc.txt ├── sample ├── __init__.py ├── method │ └── random_move_method.py └── usage │ ├── __init__.py │ ├── problem │ ├── __init__.py │ └── matching_problem_generator.py │ ├── pulp │ ├── whiskas.py │ └── whiskas_pulp.py │ └── sample │ ├── __init__.py │ ├── basic.py │ ├── how_to_use_delta_objective.py │ ├── how_to_use_init_answers.py │ ├── how_to_use_user_define_constraint.py │ ├── knapsack.py │ ├── marketing.py │ ├── matching.py │ ├── mdcvrp.py │ ├── sharing_clustering.py │ ├── tsp.py │ └── tsp2.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # editor 2 | .idea/ 3 | .vscode/ 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Python ### 34 | # Byte-compiled / optimized / DLL files 35 | __pycache__/ 36 | *.py[cod] 37 | *$py.class 38 | 39 | # C extensions 40 | *.so 41 | 42 | # Distribution / packaging 43 | .Python 44 | build/ 45 | develop-eggs/ 46 | dist/ 47 | downloads/ 48 | eggs/ 49 | .eggs/ 50 | lib/ 51 | lib64/ 52 | parts/ 53 | sdist/ 54 | var/ 55 | wheels/ 56 | *.egg-info/ 57 | .installed.cfg 58 | *.egg 59 | MANIFEST 60 | 61 | # PyInstaller 62 | # Usually these files are written by a python script from a template 63 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 64 | *.manifest 65 | *.spec 66 | 67 | # Installer logs 68 | pip-log.txt 69 | pip-delete-this-directory.txt 70 | 71 | # Unit test / coverage reports 72 | htmlcov/ 73 | .tox/ 74 | .coverage 75 | .coverage.* 76 | .cache 77 | nosetests.xml 78 | coverage.xml 79 | *.cover 80 | .hypothesis/ 81 | .pytest_cache/ 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | docs/.doctrees/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 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 | 117 | 118 | ### Python.VirtualEnv Stack ### 119 | # Virtualenv 120 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 121 | [Bb]in 122 | [Ll]ib 123 | [Ll]ib64 124 | [Ll]ocal 125 | [Ss]cripts 126 | pyvenv.cfg 127 | pip-selfcheck.json 128 | 129 | # ignore gitkeep 130 | !.gitkeep 131 | .vscode/settings.json 132 | 133 | 134 | # CMake 135 | *.dSYM 136 | *.dtps 137 | CMakeFiles 138 | Makefile 139 | CMakeCache.txt 140 | _build/ 141 | _generate/ 142 | cmake-build-debug/ 143 | _skbuild -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Recruit Communications Co., Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include pyproject.toml 3 | include setup.py 4 | 5 | recursive-include codableopt * 6 | recursive-include sample * -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | scikit-learn = "*" 8 | pandas = "*" 9 | pulp = "*" 10 | geopy = "*" 11 | folium = "*" 12 | 13 | [packages] 14 | numpy = "==1.21.0" 15 | loky = "==3.3.0" 16 | 17 | [requires] 18 | python_version = "3.7" 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/codableopt.svg 2 | :target: https://pypi.python.org/pypi/codableopt 3 | 4 | .. image:: https://readthedocs.org/projects/codable-model-optimizer/badge/?version=latest 5 | :target: https://codable-model-optimizer.readthedocs.io/ja/latest/?badge=latest 6 | :alt: Documentation Status 7 | 8 | 9 | 10 | ========================= 11 | codable-model-optimizer 12 | ========================= 13 | Optimization problem meta-heuristics solver for easy modeling. 14 | 15 | .. index-start-installation-marker 16 | 17 | Installation 18 | ================ 19 | 20 | Use pip 21 | ------- 22 | 23 | .. code-block:: bash 24 | 25 | $ pip install codableopt 26 | 27 | Use setup.py 28 | ------------ 29 | 30 | .. code-block:: bash 31 | 32 | # Master branch 33 | $ git clone https://github.com/recruit-tech/codable-model-optimizer 34 | $ python3 setup.py install 35 | 36 | 37 | 38 | .. index-end-installation-marker 39 | 40 | Example Usage 41 | ================= 42 | 43 | Sample1 44 | ------------------- 45 | 46 | .. index-start-sample1 47 | 48 | .. code-block:: python 49 | 50 | import numpy as np 51 | from codableopt import * 52 | 53 | # set problem 54 | problem = Problem(is_max_problem=True) 55 | 56 | # define variables 57 | x = IntVariable(name='x', lower=np.double(0), upper=np.double(5)) 58 | y = DoubleVariable(name='y', lower=np.double(0.0), upper=None) 59 | z = CategoryVariable(name='z', categories=['a', 'b', 'c']) 60 | 61 | 62 | # define objective function 63 | def objective_function(var_x, var_y, var_z, parameters): 64 | obj_value = parameters['coef_x'] * var_x + parameters['coef_y'] * var_y 65 | 66 | if var_z == 'a': 67 | obj_value += 10.0 68 | elif var_z == 'b': 69 | obj_value += 8.0 70 | else: 71 | # var_z == 'c' 72 | obj_value -= 3.0 73 | 74 | return obj_value 75 | 76 | 77 | # set objective function and its arguments 78 | problem += Objective(objective=objective_function, 79 | args_map={'var_x': x, 80 | 'var_y': y, 81 | 'var_z': z, 82 | 'parameters': {'coef_x': -3.0, 'coef_y': 4.0}}) 83 | 84 | # define constraint 85 | problem += 2 * x + 4 * y + 2 * (z == 'a') + 3 * (z == ('b', 'c')) <= 8 86 | problem += 2 * x - y + 2 * (z == 'b') > 3 87 | 88 | print(problem) 89 | 90 | solver = OptSolver() 91 | 92 | # generate optimization methods to be used within the solver 93 | method = PenaltyAdjustmentMethod(steps=40000) 94 | 95 | answer, is_feasible = solver.solve(problem, method) 96 | print(f'answer:{answer}, answer_is_feasible:{is_feasible}') 97 | 98 | .. index-end-sample1 99 | 100 | Sample2 101 | ------------------- 102 | 103 | 104 | .. code-block:: python 105 | 106 | import random 107 | from itertools import combinations 108 | 109 | from codableopt import Problem, Objective, CategoryVariable, OptSolver, PenaltyAdjustmentMethod 110 | 111 | 112 | # define distance generating function 113 | def generate_distances(args_place_names): 114 | generated_distances = {} 115 | for point_to_point in combinations(['start'] + args_place_names, 2): 116 | distance_value = random.randint(20, 40) 117 | generated_distances[point_to_point] = distance_value 118 | generated_distances[tuple(reversed(point_to_point))] = distance_value 119 | for x in ['start'] + args_place_names: 120 | generated_distances[(x, x)] = 0 121 | 122 | return generated_distances 123 | 124 | 125 | # generate TSP problem 126 | PLACE_NUM = 30 127 | destination_names = [f'destination_{no}' for no in range(PLACE_NUM)] 128 | place_names = [f'P{no}' for no in range(PLACE_NUM)] 129 | distances = generate_distances(place_names) 130 | destinations = [CategoryVariable(name=destination_name, categories=place_names) 131 | for destination_name in destination_names] 132 | 133 | # set problem 134 | problem = Problem(is_max_problem=False) 135 | 136 | 137 | # define objective function 138 | def calc_distance(var_destinations, para_distances): 139 | return sum([para_distances[(x, y)] for x, y in zip( 140 | ['start'] + var_destinations, var_destinations + ['start'])]) 141 | 142 | 143 | # set objective function and its arguments 144 | problem += Objective(objective=calc_distance, 145 | args_map={'var_destinations': destinations, 'para_distances': distances}) 146 | 147 | # define constraint 148 | # constraint formula that always reaches all points at least once 149 | for place_name in place_names: 150 | problem += sum([(destination == place_name) for destination in destinations]) >= 1 151 | 152 | # optimization implementation 153 | solver = OptSolver(round_times=4, debug=True, debug_unit_step=1000) 154 | method = PenaltyAdjustmentMethod(steps=10000, delta_to_update_penalty_rate=0.9) 155 | answer, is_feasible = solver.solve(problem, method, n_jobs=-1) 156 | 157 | print(f'answer_is_feasible:{is_feasible}') 158 | root = ['start'] + [answer[root] for root in destination_names] + ['start'] 159 | print(f'root: {" -> ".join(root)}') 160 | -------------------------------------------------------------------------------- /codableopt/__init__.py: -------------------------------------------------------------------------------- 1 | from codableopt.interface.interface import Problem, Objective, IntVariable, DoubleVariable, \ 2 | CategoryVariable, UserDefineConstraint 3 | from codableopt.solver.opt_solver import OptSolver 4 | from codableopt.solver.optimizer.method.penalty_adjustment_method import PenaltyAdjustmentMethod 5 | -------------------------------------------------------------------------------- /codableopt/interface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/interface/__init__.py -------------------------------------------------------------------------------- /codableopt/interface/system_args_map.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List, Dict, Union, Any 16 | 17 | 18 | class SystemArgsMap: 19 | """引数のマッピング情報クラス。 20 | """ 21 | 22 | def __init__( 23 | self, 24 | variable_args_map: Dict[Any, Union[List[str], List[list]]], 25 | category_args_map: Dict[str, List[str]], 26 | parameter_args_map: Dict[str, Any]): 27 | self._variable_args_map = variable_args_map 28 | self._category_args_map = category_args_map 29 | self._parameter_args_map = parameter_args_map 30 | 31 | @property 32 | def variable_args_map(self) -> Dict[Any, Union[List[str], List[list]]]: 33 | return self._variable_args_map 34 | 35 | @property 36 | def category_args_map(self) -> Dict[str, List[str]]: 37 | return self._category_args_map 38 | 39 | @property 40 | def parameter_args_map(self) -> Dict[str, Any]: 41 | return self._parameter_args_map 42 | -------------------------------------------------------------------------------- /codableopt/interface/system_constraint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict, List 16 | 17 | import numpy as np 18 | 19 | from codableopt.interface.system_args_map import SystemArgsMap 20 | 21 | 22 | class SystemConstraint: 23 | """制約式のベースクラス。 24 | """ 25 | 26 | def __init__(self): 27 | pass 28 | 29 | 30 | class SystemLinerConstraint(SystemConstraint): 31 | """線形制約式のクラス。 32 | 33 | Ax + b >= 0 or Ax + b > 0 34 | A is variable_coefficients 35 | b is constant 36 | (if include_equal_to_zero is True) Ax + b >= 0 (else) Ax + b > 0 37 | """ 38 | 39 | def __init__(self, 40 | var_coefficients: Dict[str, np.double], 41 | constant: np.double, 42 | include_equal_to_zero: bool): 43 | self._var_coefficients = var_coefficients 44 | self._constant = constant 45 | self._include_equal_to_zero = include_equal_to_zero 46 | super().__init__() 47 | 48 | def to_string(self) -> str: 49 | """線形制約式を文字列に変換。 50 | 51 | Returns: 52 | 線形制約式の文字列 53 | """ 54 | formula_str = \ 55 | ' '.join([f'{"+" if val >= 0 and no > 0 else ""}{val}*{name}' 56 | for no, (name, val) 57 | in enumerate(self._var_coefficients.items())]) 58 | formula_str += \ 59 | f' {"+" if self._constant >= 0 else ""} {self._constant}' 60 | 61 | if self._include_equal_to_zero: 62 | return formula_str + ' >= 0' 63 | else: 64 | return formula_str + ' > 0' 65 | 66 | @property 67 | def var_coefficients(self) -> Dict[str, np.double]: 68 | return self._var_coefficients 69 | 70 | @property 71 | def constant(self) -> np.double: 72 | return self._constant 73 | 74 | @property 75 | def include_equal_to_zero(self) -> bool: 76 | return self._include_equal_to_zero 77 | 78 | 79 | class SystemUserDefineConstraint(SystemConstraint): 80 | """ユーザ定義の制約式のクラス。(利用非推奨) 81 | """ 82 | 83 | def __init__(self, constraint_function, system_args_map: SystemArgsMap): 84 | 85 | self._constraint_function = constraint_function 86 | self._args_map = system_args_map 87 | super().__init__() 88 | 89 | @property 90 | def constraint_function(self): 91 | return self._constraint_function 92 | 93 | @property 94 | def args_map(self): 95 | return self._args_map 96 | 97 | 98 | class SystemConstraints: 99 | """問題に設定されている制約式をまとめたクラス。 100 | """ 101 | 102 | def __init__(self, 103 | liner_constraints: List[SystemLinerConstraint], 104 | user_define_constraints: List[SystemUserDefineConstraint]): 105 | self._liner_constraints = liner_constraints 106 | self._user_define_constraints = user_define_constraints 107 | 108 | @property 109 | def liner_constraints(self) -> List[SystemLinerConstraint]: 110 | return self._liner_constraints 111 | 112 | @property 113 | def user_define_constraints(self) -> List[SystemUserDefineConstraint]: 114 | return self._user_define_constraints 115 | -------------------------------------------------------------------------------- /codableopt/interface/system_objective.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict, Optional, Union, List, Any 16 | 17 | import numpy as np 18 | 19 | from codableopt.interface.system_args_map import SystemArgsMap 20 | 21 | 22 | class SystemObjective: 23 | """目的関数のベースクラス。 24 | """ 25 | 26 | def __init__(self, is_max_problem: bool, exist_delta_function: bool): 27 | """目的関数を設定するオブジェクト生成関数。 28 | 29 | Args: 30 | is_max_problem: 最適化問題が最大化問題であるフラグ(Trueの場合は最大化問題、Falseの場合は最小化問題) 31 | exist_delta_function: 差分計算の利用有無のフラグ 32 | """ 33 | self._is_max_problem = is_max_problem 34 | self._exist_delta_function = exist_delta_function 35 | 36 | def calc_objective(self, answer) -> np.double: 37 | """目的関数の値を計算する関数。 38 | 39 | Args: 40 | answer: 解答 41 | 42 | Returns: 43 | 目的関数の値 44 | """ 45 | raise NotImplementedError('You must write calculate_objective_function!') 46 | 47 | def calc_delta_objective(self, answer) -> np.double: 48 | """目的関数の値を差分計算によって計算する関数。 49 | 50 | Args: 51 | answer: 解答 52 | 53 | Returns: 54 | 目的関数の値 55 | """ 56 | raise NotImplementedError( 57 | 'Write calculate_objective_function_by_difference_calculation!') 58 | 59 | @property 60 | def is_max_problem(self) -> bool: 61 | return self._is_max_problem 62 | 63 | @property 64 | def exist_delta_function(self) -> bool: 65 | return self._exist_delta_function 66 | 67 | 68 | class SystemUserDefineObjective(SystemObjective): 69 | """ユーザ定義する目的関数クラス。 70 | """ 71 | 72 | def __init__( 73 | self, 74 | is_max_problem: bool, 75 | objective, 76 | delta_objective: Optional, 77 | system_args_map: SystemArgsMap): 78 | self._objective = objective 79 | self._delta_objective = delta_objective 80 | self._system_args_map = system_args_map 81 | super().__init__(is_max_problem, delta_objective is not None) 82 | 83 | def calc_objective(self, args: Dict[str, Any]) -> np.double: 84 | return self._objective(**args) 85 | 86 | def calc_delta_objective(self, args: Dict[str, Any]) -> np.double: 87 | return self._delta_objective(**args) 88 | 89 | @property 90 | def args_map(self) -> SystemArgsMap: 91 | return self._system_args_map 92 | 93 | @property 94 | def variable_args_map(self) -> Dict[str, Union[str, List[str]]]: 95 | return self._system_args_map.variable_args_map 96 | 97 | @property 98 | def category_args_map(self) -> Dict[str, Union[str, List[str]]]: 99 | return self._system_args_map.category_args_map 100 | 101 | @property 102 | def parameter_args_map(self) -> Dict[str, Union[str, List[str]]]: 103 | return self._system_args_map.parameter_args_map 104 | -------------------------------------------------------------------------------- /codableopt/interface/system_problem.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List 16 | 17 | from codableopt.interface.system_variable import SystemVariable 18 | from codableopt.interface.system_objective import SystemObjective 19 | from codableopt.interface.system_constraint import SystemConstraints 20 | 21 | 22 | class SystemProblem: 23 | """最適化問題のクラス。 24 | """ 25 | 26 | def __init__( 27 | self, 28 | variables: List[SystemVariable], 29 | objective: SystemObjective, 30 | constraints: SystemConstraints): 31 | """最適化問題のオブジェクト生成関数。 32 | 33 | Args: 34 | variables: 最適化問題の変数リスト 35 | objective: 最適化問題の目的関数 36 | constraints: 最適化問題の制約式集合 37 | """ 38 | self._variables = variables 39 | self._objective = objective 40 | self._constraints = constraints 41 | 42 | @property 43 | def variables(self) -> List[SystemVariable]: 44 | return self._variables 45 | 46 | @property 47 | def objective(self) -> SystemObjective: 48 | return self._objective 49 | 50 | @property 51 | def constraints(self) -> SystemConstraints: 52 | return self._constraints 53 | -------------------------------------------------------------------------------- /codableopt/interface/system_variable.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Optional, List 16 | from abc import ABC, abstractmethod 17 | 18 | import numpy as np 19 | 20 | from codableopt.interface.system_constraint import SystemLinerConstraint 21 | 22 | 23 | class SystemVariable(ABC): 24 | """最適化問題の変数のベースクラス。 25 | """ 26 | 27 | def __init__(self, name: str): 28 | """最適化問題の変数のオブジェクト生成関数。 29 | 30 | Args: 31 | name: 変数名 32 | """ 33 | self._name = name 34 | 35 | @abstractmethod 36 | def to_constraints_of_range(self) -> List[SystemLinerConstraint]: 37 | """変数の上界値と下界値を線形制約式に変換する関数。 38 | 39 | Returns: 40 | 上界値と下界値の線形制約式リスト。 41 | """ 42 | raise NotImplementedError('to_variable_limit_constraints is not implemented!') 43 | 44 | @abstractmethod 45 | def extract_coefficients( 46 | self, 47 | constraint: SystemLinerConstraint) -> List[float]: 48 | """引数の線形制約式から呼び出し元オブジェクトの変数の係数を取得する関数。 49 | 50 | Args: 51 | constraint: 52 | 線形制約式のオブジェクト。 53 | 54 | Returns: 55 | 呼び出し元オブジェクトの変数の線形制約式の係数 56 | """ 57 | raise NotImplementedError('to_constraint_coefficients is not implemented!') 58 | 59 | @property 60 | def name(self) -> str: 61 | return self._name 62 | 63 | 64 | class SystemIntegerVariable(SystemVariable): 65 | """整数型の変数クラス。 66 | """ 67 | 68 | def __init__( 69 | self, 70 | name: str, 71 | lower: Optional[np.double], 72 | upper: Optional[np.double]): 73 | super().__init__(name) 74 | self._lower = lower 75 | self._upper = upper 76 | 77 | def to_constraints_of_range(self) -> List[SystemLinerConstraint]: 78 | constant_list = [] 79 | if self._lower is not None: 80 | constant = SystemLinerConstraint( 81 | var_coefficients={self._name: np.double(1.0)}, 82 | constant=np.double(-self._lower), 83 | include_equal_to_zero=True 84 | ) 85 | constant_list.append(constant) 86 | if self._upper is not None: 87 | constant = SystemLinerConstraint( 88 | var_coefficients={self._name: np.double(-1.0)}, 89 | constant=self._upper, 90 | include_equal_to_zero=True 91 | ) 92 | constant_list.append(constant) 93 | 94 | return constant_list 95 | 96 | def extract_coefficients( 97 | self, 98 | constraint: SystemLinerConstraint) -> List[float]: 99 | if self._name in constraint.var_coefficients.keys(): 100 | return [constraint.var_coefficients[self._name]] 101 | else: 102 | return [0.0] 103 | 104 | @property 105 | def lower(self) -> Optional[np.double]: 106 | return self._lower 107 | 108 | @property 109 | def upper(self) -> Optional[np.double]: 110 | return self._upper 111 | 112 | 113 | class SystemDoubleVariable(SystemVariable): 114 | """少数型の変数クラス。 115 | """ 116 | 117 | def __init__( 118 | self, 119 | name: str, 120 | lower: Optional[np.double], 121 | upper: Optional[np.double]): 122 | super().__init__(name) 123 | self._lower = lower 124 | self._upper = upper 125 | 126 | def to_constraints_of_range(self) -> List[SystemLinerConstraint]: 127 | constant_list = [] 128 | if self._lower is not None: 129 | constant = SystemLinerConstraint( 130 | var_coefficients={self._name: np.double(1.0)}, 131 | constant=np.double(-self._lower), 132 | include_equal_to_zero=True) 133 | constant_list.append(constant) 134 | if self._upper is not None: 135 | constant = SystemLinerConstraint( 136 | var_coefficients={self._name: np.double(-1.0)}, 137 | constant=self._upper, 138 | include_equal_to_zero=True) 139 | constant_list.append(constant) 140 | 141 | return constant_list 142 | 143 | def extract_coefficients( 144 | self, 145 | constraint: SystemLinerConstraint) -> List[float]: 146 | if self._name in constraint.var_coefficients.keys(): 147 | return [constraint.var_coefficients[self._name]] 148 | else: 149 | return [0.0] 150 | 151 | @property 152 | def lower(self) -> Optional[np.double]: 153 | return self._lower 154 | 155 | @property 156 | def upper(self) -> Optional[np.double]: 157 | return self._upper 158 | 159 | 160 | class SystemCategoryVariable(SystemVariable): 161 | """カテゴリ型の変数クラス。 162 | """ 163 | 164 | def __init__(self, name: str, categories: List[str]): 165 | super().__init__(name) 166 | self._categories = categories 167 | self._category_num = len(categories) 168 | 169 | def to_constraints_of_range(self) -> List[SystemLinerConstraint]: 170 | return [] 171 | 172 | def extract_coefficients( 173 | self, 174 | constraint: SystemLinerConstraint) -> List[float]: 175 | return [constraint.var_coefficients[f'{self.name}:{x}'] 176 | if (f'{self._name}:{x}' in constraint.var_coefficients.keys()) else 0.0 177 | for x in self._categories] 178 | 179 | @property 180 | def categories(self) -> List[str]: 181 | return self._categories 182 | 183 | @property 184 | def category_num(self) -> int: 185 | return self._category_num 186 | -------------------------------------------------------------------------------- /codableopt/package_info.py: -------------------------------------------------------------------------------- 1 | # (major, minor, patch, prerelease) 2 | VERSION = (0, 1, 4, '') 3 | __shortversion__ = '.'.join(map(str, VERSION[:3])) 4 | __version__ = '.'.join(map(str, VERSION[:3])) + ''.join(VERSION[3:]) 5 | 6 | __package_name__ = 'codableopt' 7 | __author_names__ = 'Tomomitsu Motohashi, Kotaro Tanahashi' 8 | __author_emails__ = 'tomomoto1983@gmail.com, tanahashi@r.recruit.co.jp' 9 | __maintainer_names__ = 'Kotaro Tanahashi' 10 | __maintainer_emails__ = 'tanahashi@r.recruit.co.jp' 11 | __homepage__ = '' 12 | __repository_url__ = 'https://github.com/recruit-tech/codable-model-optimizer' 13 | __download_url__ = 'https://github.com/recruit-tech/codable-model-optimizer' 14 | __description__ = 'Optimization problem meta-heuristics solver for easy modeling.' 15 | __license__ = 'Apache 2.0' 16 | __keywords__ = 'optimization, solver, modeling, meta-heuristics, mathematical optimization' 17 | -------------------------------------------------------------------------------- /codableopt/solver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/formulation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/formulation/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/formulation/args_map/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/formulation/args_map/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/formulation/args_map/solver_args_map.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List 16 | 17 | from codableopt.solver.formulation.variable.solver_variable import SolverVariable 18 | from codableopt.solver.formulation.variable.solver_category_variable import SolverCategoryVariable 19 | from codableopt.interface.system_args_map import SystemArgsMap 20 | 21 | 22 | class SolverArgsMap: 23 | """Solverための引数マッピング情報のクラス。 24 | """ 25 | 26 | def __init__( 27 | self, 28 | args_map: SystemArgsMap, 29 | solver_variables: List[SolverVariable]): 30 | 31 | # TODO Refactor improve readability 32 | solver_var_dict = {x.name: x.var_index for x in solver_variables} 33 | for var in solver_variables: 34 | if isinstance(var, SolverCategoryVariable): 35 | array_indexes = [var.var_index + x for x in range(len(var.categories))] 36 | solver_var_dict[f'{var.name}'] = (array_indexes, var.categories) 37 | for category_no, category in enumerate(var.categories): 38 | solver_var_dict[f'{var.name}:{category}'] = var.var_index + category_no 39 | 40 | self._args = {} 41 | for parameter_key in args_map.parameter_args_map.keys(): 42 | self._args[parameter_key] = args_map.parameter_args_map[parameter_key] 43 | # TODO n次元対応 44 | self._single_vars_of_args = [] 45 | self._multi_vars_of_args = [] 46 | self._2d_array_vars_of_args = [] 47 | self._3d_array_vars_of_args = [] 48 | for var_key in args_map.variable_args_map.keys(): 49 | value = args_map.variable_args_map[var_key] 50 | if isinstance(value[0], list) and isinstance(value[0][0], list) and \ 51 | isinstance(value[0][0][0], list): 52 | state_indexes_and_category_var = \ 53 | [[[[solver_var_dict[x] for x in y] for y in z] for z in a] for a in value] 54 | self._3d_array_vars_of_args.append((var_key, state_indexes_and_category_var)) 55 | elif isinstance(value[0], list) and isinstance(value[0][0], list): 56 | state_indexes_and_category_var = \ 57 | [[[solver_var_dict[x] for x in y] for y in z] for z in value] 58 | self._2d_array_vars_of_args.append((var_key, state_indexes_and_category_var)) 59 | elif isinstance(value[0], list): 60 | state_indexes_and_category_var = [[solver_var_dict[x] for x in y] for y in value] 61 | self._multi_vars_of_args.append((var_key, state_indexes_and_category_var)) 62 | else: 63 | state_indexes_and_category_var = [solver_var_dict[x] for x in value] 64 | self._single_vars_of_args.append((var_key, state_indexes_and_category_var)) 65 | 66 | self._single_category_vars_of_args = [] 67 | self._multi_category_vars_of_args = [] 68 | self._2d_array_category_vars_of_args = [] 69 | self._3d_array_category_vars_of_args = [] 70 | for var_key in args_map.category_args_map.keys(): 71 | value = args_map.category_args_map[var_key] 72 | if isinstance(value[0], list) and isinstance(value[0][0], list) and \ 73 | isinstance(value[0][0][0], list): 74 | state_indexes = \ 75 | [[[[solver_var_dict[x] for x in y][0] for y in z] for z in a] for a in value] 76 | self._3d_array_category_vars_of_args.append((var_key, state_indexes)) 77 | elif isinstance(value[0], list) and isinstance(value[0][0], list): 78 | state_indexes = [[[solver_var_dict[x] for x in y][0] for y in z] for z in value] 79 | self._2d_array_category_vars_of_args.append((var_key, state_indexes)) 80 | elif isinstance(value[0], list): 81 | state_indexes = [[solver_var_dict[x] for x in y][0] for y in value] 82 | self._multi_category_vars_of_args.append((var_key, state_indexes)) 83 | else: 84 | state_indexes = [solver_var_dict[x] for x in value][0] 85 | self._single_category_vars_of_args.append((var_key, state_indexes)) 86 | 87 | def update_previous_args(self, state): 88 | self._update_args(state, head_name='pre_') 89 | 90 | def update_args(self, state): 91 | self._update_args(state, head_name='') 92 | 93 | def _update_args(self, state, head_name: str = ''): 94 | # TODO Refactor improve performance and readability 95 | for args in self._single_vars_of_args: 96 | self._args[f'{head_name}{args[0]}'] = sum(state[args[1]]) 97 | 98 | for args in self._multi_vars_of_args: 99 | self._args[f'{head_name}{args[0]}'] = [sum(state[x]) for x in args[1]] 100 | 101 | for args in self._2d_array_vars_of_args: 102 | self._args[f'{head_name}{args[0]}'] = [[sum(state[x]) for x in y] for y in args[1]] 103 | 104 | for args in self._3d_array_vars_of_args: 105 | self._args[f'{head_name}{args[0]}'] = \ 106 | [[[sum(state[x]) for x in y] for y in z] for z in args[1]] 107 | 108 | for args in self._single_category_vars_of_args: 109 | indexes, categories = args[1] 110 | self._args[f'{head_name}{args[0]}'] = \ 111 | categories[([x for x, y in enumerate(state[indexes]) if y == 1][0])] 112 | 113 | for args in self._multi_category_vars_of_args: 114 | self._args[f'{head_name}{args[0]}'] = \ 115 | [categories[([x for x, y in enumerate(state[indexes]) if y == 1][0])] 116 | for indexes, categories in args[1]] 117 | 118 | for args in self._2d_array_category_vars_of_args: 119 | self._args[f'{head_name}{args[0]}'] = \ 120 | [[categories[([x for x, y in enumerate(state[indexes]) if y == 1][0])] 121 | for indexes, categories in z] for z in args[1]] 122 | 123 | for args in self._3d_array_category_vars_of_args: 124 | self._args[f'{head_name}{args[0]}'] = \ 125 | [[[categories[([x for x, y in enumerate(state[indexes]) if y == 1][0])] 126 | for indexes, categories in z] for z in a] for a in args[1]] 127 | 128 | @property 129 | def args(self): 130 | return self._args 131 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/constraint/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/formulation/constraint/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/formulation/constraint/solver_constraints.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List, Optional 16 | 17 | import numpy as np 18 | 19 | from codableopt.solver.formulation.constraint.solver_liner_constraints \ 20 | import SolverLinerConstraints 21 | from codableopt.solver.formulation.constraint.solver_user_define_constraint \ 22 | import SolverUserDefineConstraint 23 | 24 | 25 | class SolverConstraints: 26 | 27 | def __init__( 28 | self, 29 | liner_constraints: Optional[SolverLinerConstraints], 30 | user_define_constraints: Optional[List[SolverUserDefineConstraint]]): 31 | self._liner_constraints = liner_constraints 32 | self._user_define_constraints = user_define_constraints 33 | 34 | self._init_penalty_coefficients = [] 35 | if self._liner_constraints is not None: 36 | self._init_penalty_coefficients += liner_constraints.init_penalty_coefficients 37 | if self._user_define_constraints is not None: 38 | self._init_penalty_coefficients += [x.init_penalty_coefficient 39 | for x in user_define_constraints] 40 | 41 | self._init_penalty_coefficients = np.array(self._init_penalty_coefficients) 42 | 43 | def calc_violation_amounts( 44 | self, 45 | var_values, 46 | cashed_liner_constraint_sums) -> List[np.double]: 47 | """全ての制約式の制約違反量を計算する関数。 48 | 49 | Args: 50 | var_values: 各変数の値 51 | cashed_liner_constraint_sums: 制約式の左項の合計値のキャッシュ(変数の遷移提案前の値) 52 | Returns: 53 | 全ての制約式の制約違反量 54 | """ 55 | vio_amounts = [] 56 | 57 | if self._liner_constraints is not None: 58 | vio_amounts += \ 59 | self._liner_constraints.calc_violation_amounts(cashed_liner_constraint_sums) 60 | 61 | if self._user_define_constraints is not None: 62 | for user_define_constraint in self._user_define_constraints: 63 | vio_amounts += [user_define_constraint.calc_violation_amount(var_values)] 64 | 65 | return vio_amounts 66 | 67 | @property 68 | def liner_constraints(self) -> SolverLinerConstraints: 69 | return self._liner_constraints 70 | 71 | @property 72 | def user_define_constraints(self) -> List[SolverUserDefineConstraint]: 73 | return self._user_define_constraints 74 | 75 | @property 76 | def init_penalty_coefficients(self) -> np.array: 77 | return self._init_penalty_coefficients 78 | 79 | @property 80 | def is_no_constraint(self) -> bool: 81 | return self._liner_constraints.is_no_constraint and len(self._user_define_constraints) == 0 82 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/constraint/solver_liner_constraints.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List, Dict 16 | import math 17 | 18 | import numpy as np 19 | 20 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 21 | from codableopt.interface.system_constraint import SystemLinerConstraint 22 | 23 | 24 | class SolverLinerConstraints: 25 | """Solverのための問題に含まれる全ての線形制約式を管理するクラス。 26 | """ 27 | 28 | # 制約式に=が含まれていない場合において、右項と左項の値が等しい時にペナルティ値が0より大きくするための基準値 29 | INITIAL_PENALTY = 1.0 30 | 31 | def __init__( 32 | self, 33 | liner_constraints: List[SystemLinerConstraint], 34 | coefficients): 35 | """Solverのための問題に含まれる全ての線形制約式を管理するオブジェクト生成関数。 36 | 引数の線形制約式リストを変換し、制約式ごとではなく、全ての制約式をまとめて扱えるようになる。 37 | SolverLinerConstantsは、Variableに依存しないために、constraints_coefficientsはProblem内で計算する。 38 | 39 | Args: 40 | liner_constraints (List[SystemLinerConstraint]): 線形制約式のリスト 41 | coefficients: 各制約式の各変数の係数の二次元配列 42 | """ 43 | # 制約式が存在しない場合 44 | self._no_constraints = False 45 | if len(liner_constraints) == 0: 46 | self._no_constraints = True 47 | 48 | # 各制約式の各変数の係数の二次元配列 49 | self._coefficients = coefficients 50 | # 各制約式の定数項の配列 51 | self._constants = np.array([x.constant for x in liner_constraints]) 52 | # 各制約式の制約式に等号を含むかのフラグ配列 53 | self._include_no_zero_flags = np.array([0.0 if x.include_equal_to_zero else 1.0 54 | for x in liner_constraints]) 55 | # keyが変数のインデックス番号、valueがkeyで指定された変数が含まれている制約式のインデックス番号のリスト 56 | if self._no_constraints: 57 | self._non_zero_coefficients_index_dict = {} 58 | else: 59 | self._non_zero_coefficients_index_dict: Dict[int, List[int]] = \ 60 | {x: self._coefficients[:, x] != 0 for x in range(coefficients.shape[1])} 61 | 62 | # 各制約式のペナルティ係数の配列、初期値は1.0で固定とする 63 | self._init_penalty_coefficients = [np.double(1.0) for _ in liner_constraints] 64 | 65 | def calc_violation_amounts(self, cashed_constraint_sums) -> List[np.double]: 66 | return [ 67 | 0.0 68 | if (formula_sum > 0 if include_zero else formula_sum >= 0) 69 | else (abs(formula_sum) + SolverLinerConstraints.INITIAL_PENALTY) 70 | for formula_sum, include_zero 71 | in zip(cashed_constraint_sums, self._include_no_zero_flags)] 72 | 73 | def calc_constraint_sums(self, var_values: np.array) -> np.array: 74 | """全ての制約式の左項の合計値を計算する関数。 75 | 76 | Args: 77 | var_values: 全ての制約式の左項を計算するための解答 78 | 79 | Returns: 80 | 全ての制約式の左項の合計値の配列 81 | """ 82 | if self._no_constraints: 83 | return [] 84 | 85 | return np.dot(self._coefficients, var_values) + self._constants 86 | 87 | def apply_proposal_to_constraint_sums( 88 | self, 89 | proposals: List[ProposalToMove], 90 | cashed_constraint_sums: np.array) -> np.array: 91 | """解の遷移を適用し、全ての制約式の左項の合計値を更新する関数。 92 | 93 | Args: 94 | proposals: 適用する解の遷移リスト 95 | cashed_constraint_sums: 解の遷移前の制約式の左項の合計値キャッシュ 96 | 97 | Returns: 98 | 解の遷移後の制約式の左項の合計値キャッシュ 99 | """ 100 | if self._no_constraints: 101 | return [] 102 | 103 | # deepcopyを排除する方が早いので、copyせず更新した答えを再度更新して元に戻す方法を採用 104 | constraint_sums = cashed_constraint_sums 105 | for proposal in proposals: 106 | constraint_sums += np.dot(self._coefficients[:, proposal.var_index], 107 | proposal.new_value - proposal.pre_value) 108 | 109 | return constraint_sums 110 | 111 | def cancel_proposal_to_constraint_sums( 112 | self, 113 | proposals: List[ProposalToMove], 114 | cashed_constraint_sums: np.array) -> np.array: 115 | """適用した解の遷移を元に戻し、全ての制約式の左項の合計値を更新する関数。 116 | 117 | Args: 118 | proposals: 適用する解の遷移リスト 119 | cashed_constraint_sums: 解の遷移後の制約式の左項の合計値キャッシュ 120 | 121 | Returns: 122 | 解の遷移前の制約式の左項の合計値キャッシュ 123 | """ 124 | if self._no_constraints: 125 | return [] 126 | 127 | # deepcopyを排除する方が早いので、copyせず更新した答えを再度更新して元に戻す方法を採用 128 | constraint_sums = cashed_constraint_sums 129 | for proposal in proposals: 130 | constraint_sums -= np.dot(self._coefficients[:, proposal.var_index], 131 | proposal.new_value - proposal.pre_value) 132 | 133 | return constraint_sums 134 | 135 | def calc_var_range_in_feasible( 136 | self, 137 | var_value_array: np.array, 138 | var_index: int, 139 | cashed_constraint_sums: np.array, 140 | is_int: bool): 141 | """制約式を全て満たす時に、指定した変数がとりえる値の範囲を計算する関数。 142 | 143 | Args: 144 | var_value_array: 変数の値 145 | var_index: 計算する対象の変数のインデックス番号 146 | cashed_constraint_sums: 制約式の左項の合計値キャッシュ 147 | is_int: 計算する対象の変数の整数フラグ 148 | 149 | Returns: 150 | 制約式を満たす変数の値の範囲 151 | """ 152 | non_zero_indexes = self._non_zero_coefficients_index_dict[var_index] 153 | non_zero_coefficients = self._coefficients[:, var_index][non_zero_indexes] 154 | include_no_zero_flags = self._include_no_zero_flags[non_zero_indexes] 155 | delta_threshold = np.divide( 156 | (cashed_constraint_sums 157 | - np.dot(self._coefficients[:, var_index], var_value_array[var_index])) 158 | [non_zero_indexes], 159 | non_zero_coefficients) 160 | eps = np.finfo(np.float32).eps 161 | 162 | min_upper_delta_threshold = None 163 | upper_delta_thresholds = -delta_threshold[non_zero_coefficients < 0] 164 | if len(upper_delta_thresholds) > 0: 165 | upper_delta_thresholds -= include_no_zero_flags[non_zero_coefficients < 0] * eps 166 | min_upper_delta_threshold = upper_delta_thresholds.min() 167 | 168 | max_lower_delta_threshold = None 169 | lower_delta_threshold = delta_threshold[non_zero_coefficients > 0] 170 | if len(lower_delta_threshold) > 0: 171 | lower_delta_threshold += include_no_zero_flags[non_zero_coefficients > 0] * eps 172 | max_lower_delta_threshold = lower_delta_threshold.max() 173 | 174 | if is_int and max_lower_delta_threshold is not None: 175 | max_lower_delta_threshold = math.ceil(max_lower_delta_threshold) 176 | if is_int and min_upper_delta_threshold is not None: 177 | min_upper_delta_threshold = math.floor(min_upper_delta_threshold) 178 | 179 | return max_lower_delta_threshold, min_upper_delta_threshold 180 | 181 | @property 182 | def init_penalty_coefficients(self) -> np.array: 183 | return self._init_penalty_coefficients 184 | 185 | @property 186 | def is_no_constraint(self) -> bool: 187 | return self._no_constraints 188 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/constraint/solver_user_define_constraint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | 17 | from codableopt.solver.formulation.args_map.solver_args_map import SolverArgsMap 18 | 19 | 20 | class SolverUserDefineConstraint: 21 | 22 | def __init__(self, constraint_function, args_map: SolverArgsMap): 23 | self._constraint_function = constraint_function 24 | self._args_map = args_map 25 | # ペナルティ係数、初期値は1.0で固定とする 26 | self._init_penalty_coefficient = 1.0 27 | 28 | def calc_violation_amount(self, var_values: np.array): 29 | # 引数を更新 30 | self._args_map.update_args(var_values) 31 | 32 | # 目的関数を計算 33 | return self._constraint_function(**self._args_map.args) 34 | 35 | @property 36 | def init_penalty_coefficient(self): 37 | return self._init_penalty_coefficient 38 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/objective/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/formulation/objective/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/formulation/objective/solver_objective.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List, Optional 16 | from collections.abc import Callable 17 | 18 | import numpy as np 19 | 20 | from codableopt.solver.formulation.args_map.solver_args_map import SolverArgsMap 21 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 22 | 23 | 24 | class SolverObjective: 25 | """Solverのための目的関数クラス。 26 | """ 27 | 28 | def __init__( 29 | self, 30 | objective: Callable, 31 | delta_objective: Optional[Callable], 32 | args_map: SolverArgsMap, 33 | is_max_problem: bool, 34 | exist_delta_function: bool): 35 | """Solverのための目的関数オブジェクトの生成関数。 36 | 37 | Args: 38 | objective: 目的関数値を計算する関数 39 | delta_objective: 目的関数値を差分計算によって計算する関数 40 | args_map: 目的関数の引数に参照するマップ情報 41 | is_max_problem: 最大化問題フラグ 42 | exist_delta_function: 差分計算できる目的関数の有無 43 | """ 44 | self._objective = objective 45 | self._delta_objective = delta_objective 46 | self._args_map = args_map 47 | self._is_max_problem = is_max_problem 48 | self._exist_delta_function = exist_delta_function 49 | 50 | def calc_objective( 51 | self, 52 | var_value_array: np.array, 53 | proposals: List[ProposalToMove], 54 | cashed_objective_score: Optional[np.double]) -> np.double: 55 | """目的関数値を計算する関数。 56 | 57 | Args: 58 | var_value_array: 決定変数の値リスト 59 | proposals: 目的関数値の計算前にベース解答に適用する変数の遷移提案リスト 60 | cashed_objective_score: キャッシュされている遷移前の目的関数値 61 | 62 | Returns: 63 | 目的関数値 64 | """ 65 | if not self._exist_delta_function or len(proposals) == 0 or cashed_objective_score is None: 66 | # 通常の目的関数の計算 67 | return self._calc_objective(var_value_array, proposals) 68 | else: 69 | # 差分計算による目的関数の計算 70 | return self.calc_objective_by_delta(var_value_array, proposals, cashed_objective_score) 71 | 72 | def _calc_objective( 73 | self, 74 | var_value_array: np.array, 75 | proposals: List[ProposalToMove]): 76 | # 目的関数を変更するために、一時的に値を変更 77 | for proposal in proposals: 78 | var_value_array[proposal.var_index] = proposal.new_value 79 | 80 | # 目的関数の引数を更新 81 | self._args_map.update_args(var_value_array) 82 | 83 | # 目的関数を計算 84 | score = self._objective(self._args_map.args) 85 | 86 | # 最大化問題の場合、-で最小化問題に変換 87 | if self._is_max_problem: 88 | score = -score 89 | 90 | # 目的関数を変更していた値を元に戻す 91 | for proposal in proposals: 92 | var_value_array[proposal.var_index] = proposal.pre_value 93 | 94 | return score 95 | 96 | def calc_objective_by_delta( 97 | self, 98 | var_value_array: np.array, 99 | proposals: List[ProposalToMove], 100 | cashed_objective_score: np.double) -> np.double: 101 | # 目的関数の引数の更新前の値を更新 102 | self._args_map.update_previous_args(var_value_array) 103 | 104 | # 目的関数を変更するために、一時的に値を変更 105 | for proposal in proposals: 106 | var_value_array[proposal.var_index] = proposal.new_value 107 | 108 | # 目的関数の引数を更新 109 | self._args_map.update_args(var_value_array) 110 | 111 | # 目的関数値のキャッシュ値を設定(キャッシュは目的関数のスコアとなっているのでフラグを修正) 112 | pre_objective_score = cashed_objective_score * (-1 if self._is_max_problem else 1) 113 | 114 | # 目的関数を計算 115 | score = pre_objective_score + self._delta_objective(self._args_map.args) 116 | 117 | # 最大化問題の場合、-で最小化問題に変換 118 | if self._is_max_problem: 119 | score = -score 120 | 121 | # 目的関数を変更していた値を元に戻す 122 | for proposal in proposals: 123 | var_value_array[proposal.var_index] = proposal.pre_value 124 | 125 | return score 126 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/solver_problem.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List, Optional, Dict, Any 16 | from copy import deepcopy 17 | 18 | import numpy as np 19 | 20 | from codableopt.interface.system_problem import SystemProblem 21 | from codableopt.interface.system_objective import SystemUserDefineObjective 22 | from codableopt.interface.system_variable import SystemVariable 23 | from codableopt.interface.system_constraint import SystemConstraints, SystemLinerConstraint 24 | from codableopt.solver.formulation.objective.solver_objective import SolverObjective 25 | from codableopt.solver.formulation.constraint.solver_constraints import SolverConstraints 26 | from codableopt.solver.formulation.constraint.solver_liner_constraints \ 27 | import SolverLinerConstraints 28 | from codableopt.solver.formulation.constraint.solver_user_define_constraint \ 29 | import SolverUserDefineConstraint 30 | from codableopt.solver.formulation.variable.solver_variable_factory import SolverVariableFactory 31 | from codableopt.solver.formulation.variable.solver_variable import SolverVariable 32 | from codableopt.solver.formulation.args_map.solver_args_map import SolverArgsMap 33 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 34 | 35 | 36 | class SolverProblem: 37 | """Solverのための最適化問題クラス。 38 | SolverProblemは、問題の情報だけを扱い、状態は扱わない。(値オブジェクトである。) 39 | """ 40 | 41 | NOT_MEET_INITIAL_PENALTY = 1.0 42 | 43 | def __init__(self, problem: SystemProblem): 44 | """OptimizationProblemをSolverで扱うためのオブジェクト生成関数。 45 | 46 | Args: 47 | problem (SystemProblem): 変換元の最適化問題 48 | """ 49 | 50 | # about variable 51 | solver_variable_factory = SolverVariableFactory() 52 | self._variables: List[SolverVariable] = \ 53 | [solver_variable_factory.generate(variable) for variable in problem.variables] 54 | 55 | # about constraint 56 | self._solver_constraints = SolverProblem._to_solver_constraint( 57 | problem.constraints, problem.variables, self._variables 58 | ) 59 | 60 | # about objective 61 | objective = problem.objective 62 | if isinstance(objective, SystemUserDefineObjective): 63 | self._solver_objective = SolverObjective( 64 | objective.calc_objective, 65 | objective.calc_delta_objective, 66 | SolverArgsMap(objective.args_map, self._variables), 67 | objective.is_max_problem, 68 | objective.exist_delta_function) 69 | else: 70 | raise ValueError('objective_function is only support UserDefineObjectiveFunction!') 71 | 72 | def calc_objective( 73 | self, 74 | var_value_array: np.array, 75 | proposals: List[ProposalToMove], 76 | cashed_objective_score: Optional[np.double] = None) -> np.double: 77 | """目的関数値を計算する関数。 78 | 79 | Args: 80 | var_value_array (np.array): 目的関数値を計算するベース解答 81 | proposals (List[ProposalToMove]): 目的関数値の計算前にベース解答に適用する変数の遷移提案リスト 82 | cashed_objective_score(Optional[np.double]): キャッシュされている遷移前の目的関数値 83 | Returns: 84 | 目的関数値 85 | """ 86 | # 目的関数を計算 87 | return self._solver_objective.calc_objective(var_value_array, proposals, 88 | cashed_objective_score) 89 | 90 | def calc_penalties( 91 | self, 92 | var_value_array: np.array, 93 | proposals: List[ProposalToMove], 94 | penalty_coefficients: np.array, 95 | cashed_liner_constraint_sums: np.array) -> List[np.double]: 96 | """制約ごとのペナルティ値を計算する関数。 97 | 98 | Args: 99 | var_value_array: 変数の値 100 | proposals: ペナルティ値を計算する前に適用する変数の遷移提案リスト 101 | penalty_coefficients: ペナルティ係数 102 | cashed_liner_constraint_sums: 制約式の左項の合計値のキャッシュ(変数の遷移提案前の値) 103 | 104 | Returns: 105 | ペナルティ値 106 | """ 107 | self.apply_proposal_to_liner_constraint_sums(proposals, cashed_liner_constraint_sums) 108 | vio_amounts = self.calc_violation_amounts(var_value_array, cashed_liner_constraint_sums) 109 | self.cancel_proposal_to_liner_constraint_sums(proposals, cashed_liner_constraint_sums) 110 | 111 | return [x * y for x, y in zip(vio_amounts, penalty_coefficients)] 112 | 113 | def calc_violation_amounts( 114 | self, 115 | var_value_array: np.array, 116 | cashed_liner_constraint_sums=None) -> List[np.double]: 117 | """全ての制約式の制約違反量を計算する関数。 118 | 119 | Args: 120 | var_value_array: 各変数の値 121 | cashed_liner_constraint_sums: 制約式の左項の合計値のキャッシュ(変数の遷移提案前の値) 122 | Returns: 123 | 全ての制約式の制約違反量 124 | """ 125 | liner_constraint_sums = cashed_liner_constraint_sums 126 | if liner_constraint_sums is None: 127 | liner_constraint_sums = self.calc_liner_constraint_sums(var_value_array) 128 | 129 | return self._solver_constraints.calc_violation_amounts(var_value_array, 130 | liner_constraint_sums) 131 | 132 | def calc_liner_constraint_sums(self, var_value_array: np.array) -> np.array: 133 | """全ての制約式の左項の合計値を計算する関数。 134 | 135 | Args: 136 | var_value_array (np.array): 制約式を計算する解答 137 | 138 | Returns: 139 | 全ての制約式の左項の合計値 140 | """ 141 | return self._solver_constraints.liner_constraints.calc_constraint_sums(var_value_array) 142 | 143 | def apply_proposal_to_liner_constraint_sums( 144 | self, 145 | proposals: List[ProposalToMove], 146 | cashed_constraint_sums: np.array) -> Optional[np.array]: 147 | """全ての制約式の左項の合計値に、変数の遷移提案を適用し、値を更新する関数。 148 | 149 | Args: 150 | proposals: ペナルティ値を計算する前に適用する変数の遷移提案リスト 151 | cashed_constraint_sums: 制約式の左項の合計値のキャッシュ(変数の遷移提案前の値) 152 | 153 | Returns: 154 | 更新した制約式の左項の合計値 155 | """ 156 | if self._solver_constraints.liner_constraints is None: 157 | return None 158 | 159 | return self._solver_constraints.liner_constraints\ 160 | .apply_proposal_to_constraint_sums(proposals, cashed_constraint_sums) 161 | 162 | def cancel_proposal_to_liner_constraint_sums( 163 | self, 164 | proposals: List[ProposalToMove], 165 | cashed_constraint_sums: np.array) -> Optional[np.array]: 166 | """全ての制約式の左項の合計値に、適用した変数の遷移提案をキャンセルし、値を更新する関数。 167 | 168 | Args: 169 | proposals: ペナルティ値を計算する前に適用する変数の遷移提案リスト 170 | cashed_constraint_sums: 制約式の左項の合計値のキャッシュ(変数の遷移提案前の値) 171 | 172 | Returns: 173 | 更新した制約式の左項の合計値 174 | """ 175 | if self._solver_constraints.liner_constraints is None: 176 | return None 177 | 178 | return self._solver_constraints.liner_constraints\ 179 | .cancel_proposal_to_constraint_sums(proposals, cashed_constraint_sums) 180 | 181 | def encode_answer(self, answer: Dict[str, Any]): 182 | """呼び出し元の問題オブジェクトに基づき、辞書型の変数の値をvar_value_arrayに変換して返す関数。 183 | 184 | Args: 185 | 辞書型の変数の値、keyが変数名、valueが変数の値 186 | 187 | Returns: 188 | var_value_array (np.array): 変換元の解答 189 | """ 190 | var_value_array_list = [] 191 | for variable in self._variables: 192 | if variable.name in answer.keys(): 193 | var_value_array_list.append(variable.encode(answer[variable.name])) 194 | else: 195 | raise ValueError(f'Variable:{variable.name} has no value!') 196 | 197 | return np.concatenate(var_value_array_list, axis=None) 198 | 199 | def decode_answer(self, var_value_array: np.array): 200 | """呼び出し元の問題オブジェクトに基づき、var_value_arrayを辞書型の変数の値に変換して返す関数。 201 | 202 | Args: 203 | var_value_array (np.array): 変換元の解答 204 | 205 | Returns: 206 | 辞書型の変数の値、keyが変数名、valueが変数の値 207 | """ 208 | return {variable.name: variable.decode(var_value_array) for variable in self._variables} 209 | 210 | @property 211 | def variables(self) -> List[SolverVariable]: 212 | return self._variables 213 | 214 | @property 215 | def constraints(self) -> SolverConstraints: 216 | return self._solver_constraints 217 | 218 | @property 219 | def is_no_constraint(self) -> bool: 220 | return self._solver_constraints.is_no_constraint 221 | 222 | @staticmethod 223 | def _to_solver_constraint( 224 | constraints: SystemConstraints, 225 | variables: List[SystemVariable], 226 | solver_variables: List[SolverVariable]): 227 | # about liner_constraints 228 | liner_constraints = deepcopy(constraints.liner_constraints) 229 | for variable in variables: 230 | liner_constraints.extend(variable.to_constraints_of_range()) 231 | liner_coefficients = SolverProblem._to_coefficients_of_constraints( 232 | constraints=liner_constraints, 233 | variables=variables) 234 | liner_constraints = SolverLinerConstraints(liner_constraints, liner_coefficients) 235 | 236 | # about user_define_constraints 237 | user_define_constraints = [] 238 | for sys_user_define_constraint in deepcopy(constraints.user_define_constraints): 239 | user_define_constraints.append(SolverUserDefineConstraint( 240 | sys_user_define_constraint.constraint_function, 241 | SolverArgsMap(sys_user_define_constraint.args_map, solver_variables) 242 | )) 243 | 244 | return SolverConstraints(liner_constraints, user_define_constraints) 245 | 246 | @staticmethod 247 | def _to_coefficients_of_constraints( 248 | constraints: List[SystemLinerConstraint], 249 | variables: List[SystemVariable]): 250 | """制約式と変数の上界値下界値の制約を各制約と各変数の係数の2次元配列に変換する関数。 251 | 252 | Args: 253 | constraints: 変換する制約式のリスト 254 | variables: 上界値下界値の制約を取り出す変数のリスト 255 | 256 | Returns: 257 | 各制約と各変数の係数の2次元配列 258 | """ 259 | constraints_coefficients = [] 260 | for constraint in constraints: 261 | constraint_coefficients = [] 262 | for variable in variables: 263 | constraint_coefficients.extend(variable.extract_coefficients(constraint)) 264 | 265 | constraints_coefficients.append(np.array(constraint_coefficients)) 266 | 267 | return np.array(constraints_coefficients) 268 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/variable/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/formulation/variable/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/formulation/variable/solver_category_variable.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List 16 | import random 17 | 18 | import numpy as np 19 | 20 | from codableopt.solver.formulation.variable.solver_variable import SolverVariable 21 | from codableopt.solver.optimizer.optimization_state import OptimizationState 22 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 23 | 24 | 25 | class SolverCategoryVariable(SolverVariable): 26 | 27 | def __init__( 28 | self, 29 | var_no: int, 30 | var_index: int, 31 | name: str, 32 | categories: List[str]): 33 | super(SolverCategoryVariable, self).__init__(var_no, var_index, name) 34 | self._categories = categories 35 | self._category_num = len(categories) 36 | 37 | @property 38 | def categories(self) -> List[str]: 39 | return self._categories 40 | 41 | def array_size(self) -> int: 42 | return self._category_num 43 | 44 | def propose_low_penalty_move(self, state: OptimizationState) -> List[ProposalToMove]: 45 | hot_indexes = [index for index 46 | in range(self._var_index, self._var_index + self._category_num) 47 | if state.var_array[index] == 1] 48 | if len(hot_indexes) != 1: 49 | raise ValueError(f'Category Variable is not one hot encoding.') 50 | else: 51 | hot_index = hot_indexes[0] 52 | 53 | minimum_penalty_score = None 54 | best_proposal_list_groups = [] 55 | 56 | proposal_to_cold = ProposalToMove( 57 | var_no=self._var_no, 58 | var_index=hot_index, 59 | pre_value=1, 60 | new_value=0) 61 | 62 | for new_hot_index in range(self._var_index, self._var_index + self._category_num): 63 | proposal_to_hot = ProposalToMove( 64 | var_no=self._var_no, 65 | var_index=new_hot_index, 66 | pre_value=0, 67 | new_value=1) 68 | # ペナルティスコアで比較 69 | penalty_score = state.calculate_penalties([proposal_to_cold, proposal_to_hot]) 70 | 71 | if new_hot_index == hot_index: 72 | proposal_list = [] 73 | else: 74 | proposal_list = [proposal_to_cold, proposal_to_hot] 75 | 76 | if minimum_penalty_score is None or minimum_penalty_score > penalty_score: 77 | best_proposal_list_groups = [proposal_list] 78 | minimum_penalty_score = penalty_score 79 | elif minimum_penalty_score == penalty_score: 80 | best_proposal_list_groups.append(proposal_list) 81 | 82 | return random.choice(best_proposal_list_groups) 83 | 84 | def propose_random_move_with_range(self, var_value_array: np.array, lower: np.double, upper: np.double) \ 85 | -> List[ProposalToMove]: 86 | raise NotImplementedError('propose_random_move_with_range is not implemented!') 87 | 88 | def propose_random_move(self, var_value_array: np.array) -> List[ProposalToMove]: 89 | hot_indexes = \ 90 | [index for index 91 | in range(self._var_index, self._var_index + self._category_num) 92 | if var_value_array[index] == 1] 93 | new_hot_indexes = \ 94 | [index for index 95 | in range(self._var_index, self._var_index + self._category_num) 96 | if var_value_array[index] != 1] 97 | 98 | if len(hot_indexes) != 1 or len(new_hot_indexes) == 0: 99 | raise ValueError(f'Category Variable is not one hot encoding.') 100 | 101 | hot_index = hot_indexes[0] 102 | new_hot_index = random.choice(new_hot_indexes) 103 | 104 | return [ProposalToMove( 105 | var_no=self._var_no, 106 | var_index=hot_index, 107 | pre_value=1, 108 | new_value=0), 109 | ProposalToMove( 110 | var_no=self._var_no, 111 | var_index=new_hot_index, 112 | pre_value=0, 113 | new_value=1)] 114 | 115 | def decode(self, var_value_array): 116 | array_indexes = var_value_array[self._var_index:(self._var_index + self.array_size())] 117 | category_index = [index for index, value in enumerate(array_indexes) if value == 1][0] 118 | return self._categories[category_index] 119 | 120 | def random_values(self): 121 | var_value = [0] * self._category_num 122 | var_value[random.randint(0, self._category_num - 1)] = 1 123 | return var_value 124 | 125 | def encode(self, value: int) -> np.array: 126 | if value not in self._categories: 127 | raise ValueError(f'{value} is not in categories of Variable:{self._name}!') 128 | return np.array([1.0 if category == value else 0 for category in self._categories]) 129 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/variable/solver_double_variable.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List 16 | import sys 17 | from random import randint 18 | 19 | import numpy as np 20 | 21 | from codableopt.solver.formulation.variable.solver_variable import SolverVariable 22 | from codableopt.solver.optimizer.optimization_state import OptimizationState 23 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 24 | 25 | 26 | class SolverDoubleVariable(SolverVariable): 27 | 28 | def __init__( 29 | self, 30 | var_no: int, 31 | var_index: int, 32 | name: str, 33 | lower: int, 34 | upper: int): 35 | super(SolverDoubleVariable, self).__init__(var_no, var_index, name) 36 | self._lower = lower 37 | self._upper = upper 38 | 39 | def array_size(self) -> int: 40 | return 1 41 | 42 | def propose_random_move_with_range(self, var_value_array: np.array, lower: np.double, upper: np.double) \ 43 | -> List[ProposalToMove]: 44 | prev_value = var_value_array[self._var_index] 45 | new_val = lower + (upper - lower) * np.random.rand() 46 | 47 | return [ProposalToMove( 48 | var_no=self._var_no, 49 | var_index=self._var_index, 50 | pre_value=prev_value, 51 | new_value=new_val)] 52 | 53 | def propose_low_penalty_move(self, state: OptimizationState) -> List[ProposalToMove]: 54 | if state.problem.is_no_constraint: 55 | raise ValueError('propose_low_penalty_move function need constraint to use!') 56 | 57 | prev_value = state.var_array[self._var_index] 58 | 59 | lower, upper = state.problem.constraints.liner_constraints.calc_var_range_in_feasible( 60 | state.var_array, 61 | var_index=self._var_index, 62 | cashed_constraint_sums=state.cashed_liner_constraint_sums, 63 | is_int=False) 64 | 65 | if upper is None and lower is None: 66 | raise ValueError('var no={} had no constraint!'.format(self._var_no)) 67 | 68 | elif lower is None: 69 | lower = prev_value - abs(upper - prev_value) - 1 70 | elif upper is None: 71 | upper = lower + abs(prev_value - lower) + 1 72 | 73 | if upper < lower: 74 | if upper == prev_value: 75 | new_val = lower 76 | elif lower == prev_value: 77 | new_val = upper 78 | else: 79 | base_lower, base_upper = self._lower, self._upper 80 | if upper is not None and base_lower is not None and upper < base_lower: 81 | upper = base_lower 82 | if lower is not None and base_upper is not None and lower > base_upper: 83 | lower = base_upper 84 | 85 | # 満たせない制約がある場合、より制約を満たす方に向かう値を選択する 86 | # ただし、変数の範囲制約は優先して適用する 87 | 88 | # bad score計算(制約式の違反量) 89 | proposal_to_upper = ProposalToMove( 90 | var_no=self._var_no, 91 | var_index=self._var_index, 92 | pre_value=prev_value, 93 | new_value=upper) 94 | penalty_scores_of_upper = state.calculate_penalties([proposal_to_upper]) 95 | penalty_num_of_upper = sum([1 if x > 0 else 0 for x in penalty_scores_of_upper]) 96 | 97 | proposal_to_lower = ProposalToMove( 98 | var_no=self._var_no, 99 | var_index=self._var_index, 100 | pre_value=prev_value, 101 | new_value=lower) 102 | penalty_scores_of_lower = state.calculate_penalties([proposal_to_lower]) 103 | penalty_num_of_lower = sum([1 if x > 0 else 0 for x in penalty_scores_of_lower]) 104 | 105 | if penalty_num_of_upper > penalty_num_of_lower: 106 | new_val = lower 107 | elif penalty_num_of_upper < penalty_num_of_lower: 108 | new_val = upper 109 | else: 110 | if sum(penalty_scores_of_upper) > sum( 111 | penalty_scores_of_lower): 112 | new_val = lower 113 | else: 114 | new_val = upper 115 | 116 | elif upper > lower: 117 | if upper == prev_value: 118 | if randint(1, 2) == 1: 119 | new_val = lower 120 | else: 121 | new_val = lower + (upper - lower) * np.random.rand() 122 | elif lower == prev_value: 123 | if randint(1, 2) == 1: 124 | new_val = upper 125 | else: 126 | new_val = lower + (upper - lower) * np.random.rand() 127 | else: 128 | random_num = randint(1, 3) 129 | if random_num == 1: 130 | new_val = lower 131 | elif random_num == 2: 132 | new_val = upper 133 | else: 134 | new_val = lower + (upper - lower) * np.random.rand() 135 | else: 136 | # upper == lowerのケース 137 | new_val = upper 138 | 139 | if new_val > sys.maxsize: 140 | new_val = sys.maxsize 141 | if new_val < -sys.maxsize: 142 | new_val = -sys.maxsize 143 | 144 | if self._lower is not None and new_val < self._lower: 145 | new_val = self._lower 146 | elif self._upper is not None and new_val > self._upper: 147 | new_val = self._upper 148 | 149 | if prev_value == new_val: 150 | return [] 151 | 152 | proposal = ProposalToMove( 153 | var_no=self._var_no, 154 | var_index=self._var_index, 155 | pre_value=prev_value, 156 | new_value=new_val) 157 | return [proposal] 158 | 159 | def propose_random_move(self, var_value_array: np.array) -> List[ProposalToMove]: 160 | prev_value = var_value_array[self._var_index] 161 | lower, upper = self._lower, self._upper 162 | 163 | if upper is None and lower is None: 164 | lower = -np.double(SolverVariable.VALUE_WHEN_NO_LIMIT) 165 | upper = np.double(SolverVariable.VALUE_WHEN_NO_LIMIT) 166 | elif lower is None: 167 | lower = prev_value - abs(upper - prev_value) - 1 168 | elif upper is None: 169 | upper = lower + abs(prev_value - lower) + 1 170 | 171 | if upper == prev_value: 172 | if randint(1, 2) == 1: 173 | new_val = lower 174 | else: 175 | new_val = lower + (upper - lower) * np.random.rand() 176 | 177 | elif lower == prev_value: 178 | if randint(1, 2) == 1: 179 | new_val = upper 180 | else: 181 | new_val = lower + (upper - lower) * np.random.rand() 182 | 183 | else: 184 | random_num = randint(1, 3) 185 | if random_num == 1: 186 | new_val = lower 187 | elif random_num == 2: 188 | new_val = upper 189 | else: 190 | new_val = lower + (upper - lower) * np.random.rand() 191 | 192 | if prev_value == new_val: 193 | return [] 194 | 195 | proposal = ProposalToMove( 196 | var_no=self._var_no, 197 | var_index=self._var_index, 198 | pre_value=prev_value, 199 | new_value=new_val) 200 | return [proposal] 201 | 202 | def decode(self, var_value_array): 203 | return var_value_array[self._var_index] 204 | 205 | def random_values(self): 206 | upper = self._upper 207 | lower = self._lower 208 | if lower is None: 209 | lower = -np.double(SolverVariable.VALUE_WHEN_NO_LIMIT) 210 | if upper is None: 211 | upper = np.double(SolverVariable.VALUE_WHEN_NO_LIMIT) 212 | 213 | return [lower + (upper - lower) * np.random.rand()] 214 | 215 | def encode(self, value: float) -> np.array: 216 | return np.array([value]) 217 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/variable/solver_integer_variable.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import math 16 | from typing import List 17 | 18 | import numpy as np 19 | import sys 20 | from random import randint 21 | 22 | from codableopt.solver.formulation.variable.solver_variable import SolverVariable 23 | from codableopt.solver.optimizer.optimization_state import OptimizationState 24 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 25 | 26 | 27 | class SolverIntegerVariable(SolverVariable): 28 | 29 | def __init__( 30 | self, 31 | var_no: int, 32 | var_index: int, 33 | name: str, 34 | lower: int, 35 | upper: int): 36 | super(SolverIntegerVariable, self).__init__(var_no, var_index, name) 37 | self._lower = lower 38 | self._upper = upper 39 | 40 | def array_size(self) -> int: 41 | return 1 42 | 43 | def propose_low_penalty_move(self, state: OptimizationState) -> List[ProposalToMove]: 44 | if state.problem.is_no_constraint: 45 | raise ValueError('propose_low_penalty_move function need constraint to use!') 46 | 47 | prev_value = state.var_array[self._var_index] 48 | 49 | lower, upper = state.problem.constraints.liner_constraints.calc_var_range_in_feasible( 50 | state.var_array, 51 | var_index=self._var_index, 52 | cashed_constraint_sums=state.cashed_liner_constraint_sums, 53 | is_int=True) 54 | 55 | if upper is None and lower is None: 56 | raise ValueError('var no={} had no constraint!'.format(self._var_no)) 57 | elif lower is None: 58 | lower = prev_value - abs(upper - prev_value) - 1 59 | lower = math.ceil(lower) 60 | elif upper is None: 61 | upper = lower + abs(prev_value - lower) + 1 62 | upper = math.floor(upper) 63 | 64 | if upper < lower: 65 | if upper == prev_value: 66 | new_val = lower 67 | elif lower == prev_value: 68 | new_val = upper 69 | else: 70 | base_lower, base_upper = self._lower, self._upper 71 | if upper is not None and base_lower is not None \ 72 | and upper < base_lower: 73 | upper = base_lower 74 | if lower is not None and base_upper is not None \ 75 | and lower > base_upper: 76 | lower = base_upper 77 | 78 | # 満たせない制約がある場合、より制約を満たす方に向かう値を選択する 79 | # ただし、変数の範囲制約は優先して適用する 80 | 81 | # bad score計算(制約式の違反量) 82 | proposal_to_upper = ProposalToMove( 83 | var_no=self._var_no, 84 | var_index=self._var_index, 85 | pre_value=prev_value, 86 | new_value=upper) 87 | upper_penalty_scores = state.calculate_penalties([proposal_to_upper]) 88 | penalty_num_of_upper = sum([1 if x > 0 else 0 for x in upper_penalty_scores]) 89 | 90 | proposal_to_lower = ProposalToMove( 91 | var_no=self._var_no, 92 | var_index=self._var_index, 93 | pre_value=prev_value, 94 | new_value=lower) 95 | lower_penalty_scores = state.calculate_penalties([proposal_to_lower]) 96 | penalty_num_of_lower = sum([1 if x > 0 else 0 for x in lower_penalty_scores]) 97 | 98 | if penalty_num_of_upper > penalty_num_of_lower: 99 | new_val = lower 100 | elif penalty_num_of_upper < penalty_num_of_lower: 101 | new_val = upper 102 | else: 103 | if sum(upper_penalty_scores) > sum(lower_penalty_scores): 104 | new_val = lower 105 | else: 106 | new_val = upper 107 | 108 | elif upper > lower: 109 | if upper == prev_value: 110 | if randint(1, 2) == 1 or upper - 1 == lower: 111 | new_val = lower 112 | else: 113 | new_val = randint(lower, upper - 1) 114 | 115 | elif lower == prev_value: 116 | if randint(1, 2) == 1 or upper == lower + 1: 117 | new_val = upper 118 | else: 119 | new_val = randint(lower + 1, upper) 120 | 121 | else: 122 | random_num = randint(1, 3) 123 | if random_num == 1: 124 | new_val = lower 125 | elif random_num == 2: 126 | new_val = upper 127 | else: 128 | new_val = randint(lower, upper) 129 | else: 130 | # upper == lowerのケース 131 | new_val = upper 132 | 133 | if new_val > sys.maxsize: 134 | new_val = sys.maxsize 135 | if new_val < -sys.maxsize: 136 | new_val = -sys.maxsize 137 | 138 | if self._lower is not None and new_val < self._lower: 139 | new_val = self._lower 140 | elif self._upper is not None and new_val > self._upper: 141 | new_val = self._upper 142 | 143 | if prev_value == new_val: 144 | return [] 145 | 146 | return [ProposalToMove( 147 | var_no=self._var_no, 148 | var_index=self._var_index, 149 | pre_value=prev_value, 150 | new_value=new_val)] 151 | 152 | def propose_random_move_with_range(self, var_value_array: np.array, lower: np.double, upper: np.double) \ 153 | -> List[ProposalToMove]: 154 | prev_value = var_value_array[self._var_index] 155 | new_val = randint(math.floor(lower), math.ceil(upper)) 156 | 157 | return [ProposalToMove( 158 | var_no=self._var_no, 159 | var_index=self._var_index, 160 | pre_value=prev_value, 161 | new_value=new_val)] 162 | 163 | def propose_random_move(self, var_value_array: np.array) -> List[ProposalToMove]: 164 | prev_value = var_value_array[self._var_index] 165 | lower, upper = self._lower, self._upper 166 | 167 | if upper is None and lower is None: 168 | lower = -np.double(SolverVariable.VALUE_WHEN_NO_LIMIT) 169 | upper = np.double(SolverVariable.VALUE_WHEN_NO_LIMIT) 170 | elif lower is None: 171 | lower = prev_value - abs(upper - prev_value) - 1 172 | lower = math.ceil(lower) 173 | elif upper is None: 174 | upper = lower + abs(prev_value - lower) + 1 175 | upper = math.floor(upper) 176 | 177 | if upper == prev_value: 178 | if randint(1, 2) == 1 or upper - 1 == lower: 179 | new_val = lower 180 | else: 181 | new_val = randint(lower, upper - 1) 182 | elif lower == prev_value: 183 | if randint(1, 2) == 1 or upper == lower + 1: 184 | new_val = upper 185 | else: 186 | new_val = randint(lower + 1, upper) 187 | else: 188 | random_num = randint(1, 3) 189 | if random_num == 1: 190 | new_val = lower 191 | elif random_num == 2: 192 | new_val = upper 193 | else: 194 | new_val = randint(lower, upper) 195 | 196 | if new_val > sys.maxsize: 197 | new_val = sys.maxsize 198 | if new_val < -sys.maxsize: 199 | new_val = -sys.maxsize 200 | 201 | if prev_value == new_val: 202 | return [] 203 | 204 | return [ProposalToMove( 205 | var_no=self._var_no, 206 | var_index=self._var_index, 207 | pre_value=prev_value, 208 | new_value=new_val)] 209 | 210 | def decode(self, var_value_array): 211 | return int(var_value_array[self._var_index]) 212 | 213 | def random_values(self): 214 | upper = self._upper 215 | lower = self._lower 216 | if lower is None: 217 | lower = -np.double(SolverVariable.VALUE_WHEN_NO_LIMIT) 218 | if upper is None: 219 | upper = np.double(SolverVariable.VALUE_WHEN_NO_LIMIT) 220 | 221 | return [randint(lower, upper)] 222 | 223 | def encode(self, value: int) -> np.array: 224 | return np.array([value]) 225 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/variable/solver_variable.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from abc import ABC, abstractmethod 16 | from typing import List 17 | 18 | import numpy as np 19 | 20 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 21 | from codableopt.solver.optimizer.optimization_state import OptimizationState 22 | 23 | 24 | class SolverVariable(ABC): 25 | """ソルバー用の変数クラス。 26 | """ 27 | 28 | VALUE_WHEN_NO_LIMIT = 100000 29 | 30 | def __init__( 31 | self, 32 | var_no: int, 33 | var_index: int, 34 | name: str): 35 | self._name = name 36 | self._var_no = var_no 37 | self._var_index = var_index 38 | 39 | @abstractmethod 40 | def array_size(self) -> int: 41 | """変数値をarrayに変換した時のarrayの長さを取得する関数。 42 | 43 | Returns: 44 | arrayに変換した時のarrayの長さ 45 | """ 46 | raise NotImplementedError('array_size is not implemented!') 47 | 48 | @abstractmethod 49 | def propose_low_penalty_move(self, state: OptimizationState) -> List[ProposalToMove]: 50 | """なるべくペナルティの少ない解への遷移案を取得する関数。 51 | 52 | Args: 53 | state: 最適化の計算状態 54 | 55 | Returns: 56 | 解の遷移リスト 57 | """ 58 | raise NotImplementedError('move is not implemented!') 59 | 60 | @abstractmethod 61 | def propose_random_move_with_range(self, var_value_array: np.array, lower: np.double, upper: np.double) \ 62 | -> List[ProposalToMove]: 63 | """指定された範囲の解への遷移案を取得する関数。 64 | 65 | Args: 66 | var_value_array: 現状の全変数の値を現したarray 67 | lower: 下界値 68 | upper: 上界値 69 | 70 | Returns: 71 | 解の遷移リスト 72 | """ 73 | raise NotImplementedError('propose_random_move_with_range is not implemented!') 74 | 75 | @abstractmethod 76 | def propose_random_move(self, var_value_array: np.array) -> List[ProposalToMove]: 77 | """ランダムな解への遷移案を取得する関数。 78 | 79 | Args: 80 | var_value_array: 現状の全変数の値を現したarray 81 | 82 | Returns: 83 | 解の遷移リスト 84 | """ 85 | raise NotImplementedError( 86 | 'move_on_force_to_improve is not implemented!') 87 | 88 | @abstractmethod 89 | def random_values(self): 90 | """変数のランダムな値を取得する関数。 91 | 92 | Returns: 93 | ランダムな値 94 | """ 95 | raise NotImplementedError('random_value is not implemented!') 96 | 97 | @abstractmethod 98 | def encode(self, value) -> np.array: 99 | """引数の値をエンコードして値を返す関数。 100 | 101 | Args: 102 | value: エンコード前の変数の値 103 | 104 | Returns: 105 | エンコード後の変数の値リスト 106 | """ 107 | raise NotImplementedError('decode is not implemented!') 108 | 109 | @abstractmethod 110 | def decode(self, var_value_array): 111 | """変数の値を引数の変数の値の集合から取得する関数。 112 | 113 | Args: 114 | var_value_array: 問題に含まれる全変数の値を現したarray 115 | 116 | Returns: 117 | 変数の値 118 | """ 119 | raise NotImplementedError('value is not implemented!') 120 | 121 | @property 122 | def name(self) -> str: 123 | return self._name 124 | 125 | @property 126 | def var_no(self) -> int: 127 | return self._var_no 128 | 129 | @property 130 | def var_index(self) -> int: 131 | return self._var_index 132 | -------------------------------------------------------------------------------- /codableopt/solver/formulation/variable/solver_variable_factory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from codableopt.interface.system_variable import SystemVariable, SystemIntegerVariable, SystemDoubleVariable, SystemCategoryVariable 16 | from codableopt.solver.formulation.variable.solver_variable import SolverVariable 17 | from codableopt.solver.formulation.variable.solver_integer_variable import SolverIntegerVariable 18 | from codableopt.solver.formulation.variable.solver_double_variable import SolverDoubleVariable 19 | from codableopt.solver.formulation.variable.solver_category_variable import SolverCategoryVariable 20 | 21 | 22 | class SolverVariableFactory: 23 | """SolverVariableを生成するFactoryクラス。 24 | """ 25 | 26 | def __init__(self): 27 | self._var_no = 0 28 | self._var_start_index = 0 29 | 30 | def generate(self, variable: SystemVariable) -> SolverVariable: 31 | """SolverVariableを生成する関数。 32 | 33 | Args: 34 | variable: 生成元の変数 35 | 36 | Returns: 37 | 生成したSolverVariable 38 | """ 39 | if isinstance(variable, SystemIntegerVariable): 40 | variable = SolverIntegerVariable( 41 | self._var_no, 42 | self._var_start_index, 43 | variable.name, 44 | variable.lower, 45 | variable.upper) 46 | elif isinstance(variable, SystemDoubleVariable): 47 | variable = SolverDoubleVariable( 48 | self._var_no, 49 | self._var_start_index, 50 | variable.name, 51 | variable.lower, 52 | variable.upper) 53 | elif isinstance(variable, SystemCategoryVariable): 54 | variable = SolverCategoryVariable( 55 | self._var_no, 56 | self._var_start_index, 57 | variable.name, 58 | variable.categories) 59 | else: 60 | raise NotImplementedError( 61 | f'Not support variable {variable.__class__}.') 62 | 63 | self._var_no += 1 64 | self._var_start_index += variable.array_size() 65 | 66 | return variable 67 | -------------------------------------------------------------------------------- /codableopt/solver/opt_solver.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Optional, List, Dict, Any 16 | from pathlib import Path 17 | 18 | from codableopt.interface.interface import Problem 19 | from codableopt.solver.formulation.solver_problem import SolverProblem 20 | from codableopt.solver.optimizer.optimization_solver import OptimizationSolver 21 | from codableopt.solver.optimizer.method.optimizer_method import OptimizerMethod 22 | 23 | 24 | class OptSolver: 25 | """最適化ソルバークラス。 26 | """ 27 | 28 | def __init__( 29 | self, 30 | round_times: int = 1, 31 | num_to_tune_penalty: int = 1000, 32 | num_to_select_init_answer: int = 1000, 33 | debug: bool = False, 34 | debug_unit_step: int = 1000, 35 | debug_log_file_path: Optional[Path] = None): 36 | """最適化ソルバーのオブジェクト生成関数。 37 | 38 | Args: 39 | round_times (int): 初期解を変えて、問題を解く回数 40 | num_to_tune_penalty (int): 初期のペナルティ係数を調整する際に利用する解答をランダム生成する数 41 | num_to_select_init_answer (int): 初期解を選択する時に、選択する元となる解答をランダム生成する数 42 | debug (bool): デバックprintの有無。 43 | debug_unit_step: デバックprintの表示step間隔 44 | debug_log_file_path: デバックログのファイル出力先を指定 45 | """ 46 | if num_to_tune_penalty <= 1: 47 | raise ValueError('answer_num_to_tune_penalty must be greater than 1.') 48 | 49 | if num_to_select_init_answer < round_times: 50 | raise ValueError('answer_num_to_select_init_answer ' 51 | 'must be greater than solve_times.') 52 | elif num_to_select_init_answer <= 1: 53 | raise ValueError('answer_num_to_select_init_answer must be greater than 1.') 54 | 55 | self._round_times = round_times 56 | self._num_to_tune_penalty = num_to_tune_penalty 57 | self._num_to_select_init_answer = num_to_select_init_answer 58 | self._debug = debug 59 | self._debug_unit_step = debug_unit_step 60 | self._debug_log_file_path = debug_log_file_path 61 | 62 | def solve( 63 | self, 64 | problem: Problem, 65 | method: OptimizerMethod, 66 | init_answers: Optional[List[Dict[str, Any]]] = None, 67 | penalty_strength: float = 1.0, 68 | n_jobs: int = 1): 69 | """最適化を実施する関数。 70 | 71 | Args: 72 | problem (OptimizationProblem): 最適化問題 73 | method (OptimizerMethod): 最適化手法 74 | init_answers: 初期解リスト、初期解を指定したい時に利用、初期解数だけ最適化実施される、初期解は辞書型(keyが変数名、valueが変数値)で設定 75 | penalty_strength: ペナルティ係数の強さ、大きくするほど強くなる 76 | n_jobs: 並列実行数 77 | Returns: 78 | 最適化の答え, 制約充足フラグ 79 | """ 80 | system_problem = problem.compile() 81 | solver_problem = SolverProblem(system_problem) 82 | 83 | opt_solver = OptimizationSolver( 84 | debug=self._debug, 85 | debug_step_unit=self._debug_unit_step, 86 | debug_log_file_path=self._debug_log_file_path 87 | ) 88 | answer, is_feasible = opt_solver.solve( 89 | solver_problem, 90 | method, 91 | self._round_times, 92 | self._num_to_tune_penalty, 93 | self._num_to_select_init_answer, 94 | init_answers, 95 | penalty_strength, 96 | n_jobs) 97 | 98 | return answer, is_feasible 99 | -------------------------------------------------------------------------------- /codableopt/solver/optimizer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/optimizer/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/optimizer/entity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/optimizer/entity/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/optimizer/entity/proposal_to_move.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from dataclasses import dataclass 16 | 17 | 18 | @dataclass 19 | class ProposalToMove: 20 | var_no: int 21 | var_index: int 22 | pre_value: float 23 | new_value: float 24 | 25 | def to_str(self) -> str: 26 | return f'var_index:{self.var_no},' \ 27 | f' state_index:{self.var_index},' \ 28 | f' value:{self.pre_value} -> {self.new_value}' 29 | -------------------------------------------------------------------------------- /codableopt/solver/optimizer/entity/score_info.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | class ScoreInfo: 16 | 17 | def __init__(self, objective_score: float, penalty_score: float): 18 | self._score = objective_score + penalty_score 19 | self._objective_score = objective_score 20 | self._penalty_score = penalty_score 21 | 22 | def set_scores(self, objective_score: float, penalty_score: float): 23 | self._score = objective_score + penalty_score 24 | self._objective_score = objective_score 25 | self._penalty_score = penalty_score 26 | 27 | @property 28 | def score(self) -> float: 29 | return self._score 30 | 31 | @property 32 | def objective_score(self) -> float: 33 | return self._objective_score 34 | 35 | @property 36 | def penalty_score(self) -> float: 37 | return self._penalty_score 38 | -------------------------------------------------------------------------------- /codableopt/solver/optimizer/method/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/optimizer/method/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/optimizer/method/optimizer_method.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from abc import ABC, abstractmethod 16 | from typing import List 17 | 18 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 19 | from codableopt.solver.optimizer.optimization_state import OptimizationState 20 | 21 | 22 | class OptimizerMethod(ABC): 23 | """最適化の手法を定義するクラス。 24 | """ 25 | 26 | def __init__(self, steps: int): 27 | self._steps = steps 28 | 29 | @abstractmethod 30 | def name(self) -> str: 31 | """最適化手法の名前を取得する関数 32 | 33 | Returns: 34 | 最適化手法の名前 35 | """ 36 | raise NotImplementedError('name is not implemented!') 37 | 38 | @abstractmethod 39 | def initialize_of_step(self, state: OptimizationState, step: int): 40 | """ステップの最初に呼び出される関数。 41 | 42 | Args: 43 | state: 実施中の最適化の情報オブジェクト 44 | step: ステップ数 45 | """ 46 | raise NotImplementedError('setup_for_step is not implemented!') 47 | 48 | @abstractmethod 49 | def propose(self, state: OptimizationState, step: int) -> List[ProposalToMove]: 50 | """解の遷移を提案する関数。 51 | 52 | Args: 53 | state: 実施中の最適化の情報オブジェクト 54 | step: ステップ数 55 | 56 | Returns: 57 | 提案する解の遷移リスト 58 | """ 59 | raise NotImplementedError('propose_moving is not implemented!') 60 | 61 | @abstractmethod 62 | def judge(self, state: OptimizationState, step: int) -> bool: 63 | """解の遷移を判定する関数。 64 | 65 | Args: 66 | state: 実施中の最適化の情報オブジェクト 67 | step: ステップ数 68 | 69 | Returns: 70 | 遷移判定結果 71 | """ 72 | raise NotImplementedError('judge_move is not implemented!') 73 | 74 | @abstractmethod 75 | def finalize_of_step(self, state: OptimizationState, step: int): 76 | """ステップの最後に呼び出される関数。 77 | 78 | Args: 79 | state: 実施中の最適化の情報オブジェクト 80 | step: ステップ数 81 | """ 82 | raise NotImplementedError('finalize_of_step is not implemented!') 83 | 84 | @property 85 | def steps(self) -> int: 86 | return self._steps 87 | -------------------------------------------------------------------------------- /codableopt/solver/optimizer/method/penalty_adjustment_method.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import random 16 | from typing import List, Optional, Dict, Tuple 17 | from random import choice 18 | 19 | import numpy as np 20 | 21 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 22 | from codableopt.solver.optimizer.optimization_state import OptimizationState 23 | from codableopt.solver.optimizer.method.optimizer_method import OptimizerMethod 24 | from codableopt.solver.formulation.variable.solver_integer_variable import SolverIntegerVariable 25 | from codableopt.solver.formulation.variable.solver_double_variable import SolverDoubleVariable 26 | 27 | 28 | class PenaltyAdjustmentMethod(OptimizerMethod): 29 | 30 | def __init__( 31 | self, 32 | steps: int, 33 | proposed_rate_of_random_movement: float = 0.95, 34 | delta_to_update_penalty_rate: float = 0.2, 35 | steps_threshold_to_judge_local_solution: Optional[int] = 100, 36 | history_value_size: int = 5, 37 | range_std_rate: float = 3.0): 38 | """ペナルティ係数調整手法の生成。 39 | 40 | Args: 41 | steps: 最適化の計算を繰り返すステップ数 42 | proposed_rate_of_random_movement: 解の遷移を提案するときにランダムな遷移を提案する割合 43 | delta_to_update_penalty_rate: ペナルティ係数を変えるときの割合(0.5を設定した場合、ペナルティを強くする時は係数を1+0.5倍、ペナルティを弱くする時は係数を1-0.5倍する) 44 | steps_threshold_to_judge_local_solution: 局所解とみなすために必要な連続で解が遷移しないステップ数(局所解とみなされた時にペナルティ係数を調整する) 45 | history_value_size: 数値が範囲指定のランダム遷移をする際に参考とする直近のデータ履歴のデータ件数 46 | range_std_rate: 数値が範囲指定のランダム遷移をする際に平均値から何倍の標準偏差まで離れている範囲を対象とするか指定する値(1.5なら、「平均値 - 1.5 * 標準偏差値」から「平均値 + 1.5 * 標準偏差値」を範囲とする) 47 | """ 48 | # 設定パラメータ 49 | super().__init__(steps) 50 | 51 | if delta_to_update_penalty_rate <= 0 or delta_to_update_penalty_rate >= 1.0: 52 | raise ValueError('delta_to_update_penalty_rate must be ' 53 | '"0 < delta_to_update_penalty_rate < 1"') 54 | 55 | self._random_movement_rate = proposed_rate_of_random_movement 56 | self._delta_penalty_rate = delta_to_update_penalty_rate 57 | self._steps_threshold = steps_threshold_to_judge_local_solution 58 | 59 | # Method内変数 60 | self._proposal_list: List[ProposalToMove] = [] 61 | # 直前のペナルティ係数の更新フラグ 62 | self._changed_penalty_flg: bool = False 63 | # 局所解に達してからのステップ数 64 | self._steps_while_not_improve_score: int = 0 65 | 66 | # 数値のmoveの範囲指定時のパラメータ関連 67 | self._history_value_size = history_value_size 68 | self._range_std_rate = range_std_rate 69 | # 数値変数の履歴 70 | self._number_variables_history: Dict[str, Tuple[np.array, np.double, np.double]] = {} 71 | 72 | def name(self) -> str: 73 | return f'penalty_adjustment_method,' \ 74 | f'steps:{self._steps},' \ 75 | f'steps_threshold:{self._steps_threshold}' 76 | 77 | def initialize_of_step(self, state: OptimizationState, step: int): 78 | pass 79 | 80 | def propose(self, state: OptimizationState, step: int) -> List[ProposalToMove]: 81 | while True: 82 | variable = choice(state.problem.variables) 83 | if random.random() <= self._random_movement_rate or state.problem.is_no_constraint: 84 | if isinstance(variable, (SolverIntegerVariable, SolverDoubleVariable)): 85 | if variable.name not in self._number_variables_history.keys(): 86 | self._proposal_list = variable.propose_random_move(state.var_array) 87 | else: 88 | value_array, average, std = self._number_variables_history[variable.name] 89 | if std == 0: 90 | self._proposal_list = variable.propose_random_move(state.var_array) 91 | else: 92 | # 平均+-標準偏差値のn倍の範囲から探索 93 | self._proposal_list = variable.propose_random_move_with_range( 94 | state.var_array, 95 | lower=np.double(average - self._range_std_rate * std), 96 | upper=np.double(average + self._range_std_rate * std)) 97 | else: 98 | self._proposal_list = variable.propose_random_move(state.var_array) 99 | else: 100 | self._proposal_list = variable.propose_low_penalty_move(state) 101 | 102 | if len(self._proposal_list) > 0: 103 | return self._proposal_list 104 | 105 | def judge(self, state: OptimizationState, step: int) -> bool: 106 | if self._changed_penalty_flg: 107 | # ペナルティ係数が変わっているので計算しなおし、解の遷移は空のリストとする 108 | penalty_scores = state.calculate_penalties(proposals=[]) 109 | state.previous_score.set_scores( 110 | state.previous_score.objective_score, sum(penalty_scores)) 111 | self._changed_penalty_flg = False 112 | 113 | # 移動判定 114 | delta_energy = state.current_score.score - state.previous_score.score 115 | move_flg = delta_energy < 0.0 116 | 117 | # 局所解判定 118 | if delta_energy >= 0: 119 | self._steps_while_not_improve_score += 1 120 | else: 121 | self._steps_while_not_improve_score = 0 122 | 123 | # 整数値、連続値の変更履歴を取得 124 | if move_flg and len(self._proposal_list) == 1: 125 | propose = self._proposal_list[0] 126 | variable = state.problem.variables[propose.var_no] 127 | if variable.name in self._number_variables_history.keys(): 128 | value_array, average, std = self._number_variables_history[variable.name] 129 | if len(value_array) > self._history_value_size: 130 | value_array[0:-1] = value_array[1:] 131 | value_array[-1] = propose.new_value 132 | else: 133 | value_array = np.append(value_array, propose.new_value) 134 | 135 | if len(value_array) == self._history_value_size: 136 | average = value_array.mean() 137 | std = value_array.std() 138 | self._number_variables_history[variable.name] = (value_array, average, std) 139 | else: 140 | self._number_variables_history[variable.name] = \ 141 | (np.array([propose.new_value]), np.double(0), np.double(0)) 142 | 143 | return move_flg 144 | 145 | def finalize_of_step(self, state: OptimizationState, step: int): 146 | # 局所解に達したら、ペナルティ係数を調整する 147 | if self._steps_while_not_improve_score >= self._steps_threshold: 148 | # ペナルティ係数を調整したらリセットする 149 | self._steps_while_not_improve_score = 0 150 | self._changed_penalty_flg = True 151 | if state.previous_score.penalty_score > 0: 152 | # 実行解がしばらく見つからない場合に満たされていない制約式のペナルティ係数を上げる 153 | 154 | # 制約式の違反量のスケールから正規化を行う 155 | vio_amounts = \ 156 | state.problem.calc_violation_amounts(state.var_array, 157 | state.cashed_liner_constraint_sums) 158 | normalized_vio_amounts = \ 159 | [vio_amount / rate for vio_amount, rate 160 | in zip(vio_amounts, state.constraints_scale)] 161 | 162 | # 制約式の違反量が最大なものをベースに割合に変換する 163 | max_normalized_vio_amounts = max(normalized_vio_amounts) 164 | if max_normalized_vio_amounts == 0: 165 | # 変更なし 166 | return 167 | 168 | normalized_vio_amount_rates = \ 169 | [x / max_normalized_vio_amounts for x in normalized_vio_amounts] 170 | 171 | # 制約式の違反量の割合を基準にペナルティ係数を上げる 172 | state.penalty_coefficients = [ 173 | penalty * (1 + self._delta_penalty_rate * rate) 174 | for penalty, rate 175 | in zip(state.penalty_coefficients, normalized_vio_amount_rates) 176 | ] 177 | 178 | else: 179 | # 実行可能解に達している場合は、全ての制約式のペナルティ係数を減らす 180 | state.penalty_coefficients = [ 181 | penalty * (1 - self._delta_penalty_rate) 182 | for penalty in state.penalty_coefficients 183 | ] 184 | -------------------------------------------------------------------------------- /codableopt/solver/optimizer/optimization_solver.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Optional, Dict, Any, List 16 | from pathlib import Path 17 | import copy 18 | import multiprocessing 19 | 20 | import numpy as np 21 | from loky import get_reusable_executor 22 | 23 | from codableopt.solver.optimizer.method.optimizer_method import OptimizerMethod 24 | from codableopt.solver.optimizer.optimizer import Optimizer 25 | from codableopt.solver.formulation.solver_problem import SolverProblem 26 | from codableopt.solver.sampler.var_value_array_sampler import VarValueArraySampler 27 | 28 | 29 | class OptimizationSolver: 30 | """最適化を複数回の実行管理する関数。 31 | """ 32 | 33 | def __init__( 34 | self, 35 | debug: bool = False, 36 | debug_step_unit: int = 1000, 37 | debug_log_file_path: Optional[Path] = None): 38 | self._debug = debug 39 | self._debug_step_unit = debug_step_unit 40 | self._debug_log_file_path = debug_log_file_path 41 | 42 | def solve( 43 | self, 44 | problem: SolverProblem, 45 | method: OptimizerMethod, 46 | round_times: int, 47 | num_to_tune_penalty: int, 48 | num_to_select_init_answer: int, 49 | init_answers: Optional[List[Dict[str, Any]]], 50 | penalty_strength: float, 51 | n_jobs: int,): 52 | """最適化を行う関数。 53 | 54 | Args: 55 | problem: 最適化問題 56 | method: 最適化手法 57 | round_times (int): 初期解を変えて、問題を解く回数 58 | num_to_tune_penalty (int): 初期のペナルティ係数を調整する際に利用する解答をランダム生成する数 59 | num_to_select_init_answer (int): 初期解を選択する時に、選択する元となる解答をランダム生成する数 60 | init_answers: 初期解リスト、初期解を指定したい時に利用、初期解数だけ最適化実施される、初期解は辞書型(keyが変数名、valueが変数値)で設定 61 | penalty_strength: ペナルティ係数の強さ、大きくするほど強くなる 62 | n_jobs: 並列実行数 63 | Returns: 64 | 最適解、制約充足フラグ 65 | """ 66 | if n_jobs == -1: 67 | n_jobs = multiprocessing.cpu_count() 68 | 69 | sampler = VarValueArraySampler() 70 | if problem.is_no_constraint: 71 | answers_to_tune = None 72 | else: 73 | answers_to_tune = sampler.generate(problem, num_to_tune_penalty) 74 | 75 | if init_answers is None: 76 | random_var_value_array_list = sampler.generate(problem, num_to_select_init_answer) 77 | init_var_value_array_list = sampler.choice(random_var_value_array_list, round_times) 78 | else: 79 | init_var_value_array_list = \ 80 | [problem.encode_answer(init_answer) for init_answer in init_answers] 81 | 82 | if n_jobs == 1: 83 | results = [] 84 | for round_no, init_var_value_array in enumerate(init_var_value_array_list): 85 | state = OptimizationSolver.__optimize( 86 | init_var_value_array=init_var_value_array, 87 | answers_to_tune=answers_to_tune, 88 | penalty_strength_to_tune=penalty_strength, 89 | method=copy.deepcopy(method), 90 | problem=copy.deepcopy(problem), 91 | round_no=round_no, 92 | debug=self._debug, 93 | debug_step_unit=self._debug_step_unit, 94 | debug_log_file_path=self._debug_log_file_path 95 | ) 96 | results.append(state) 97 | else: 98 | with get_reusable_executor(max_workers=n_jobs) as executor: 99 | tasks = [ 100 | executor.submit( 101 | OptimizationSolver.__optimize, 102 | init_var_value_array=init_var_value_array, 103 | answers_to_tune=answers_to_tune, 104 | penalty_strength_to_tune=penalty_strength, 105 | method=copy.deepcopy(method), 106 | problem=copy.deepcopy(problem), 107 | round_no=round_no, 108 | debug=self._debug, 109 | debug_step_unit=self._debug_step_unit, 110 | debug_log_file_path=self._debug_log_file_path 111 | ) 112 | for round_no, init_var_value_array in enumerate(init_var_value_array_list) 113 | ] 114 | results = [task.result() for task in tasks] 115 | 116 | best_state = None 117 | for state in results: 118 | if best_state is None: 119 | best_state = state 120 | elif not best_state.exist_feasible_answer and state.exist_feasible_answer: 121 | best_state = state 122 | elif best_state.exist_feasible_answer and state.exist_feasible_answer \ 123 | and best_state.best_score.score > state.best_score.score: 124 | best_state = state 125 | elif not best_state.exist_feasible_answer and not state.exist_feasible_answer \ 126 | and best_state.best_score.score > state.best_score.score: 127 | best_state = state 128 | 129 | return best_state.decode(best_state.best_var_array), best_state.exist_feasible_answer 130 | 131 | @staticmethod 132 | def __optimize( 133 | init_var_value_array: np.array, 134 | answers_to_tune: Optional[np.array], 135 | penalty_strength_to_tune, 136 | method: OptimizerMethod, 137 | problem: SolverProblem, 138 | round_no: int, 139 | debug: bool, 140 | debug_step_unit: int, 141 | debug_log_file_path: Optional[Path]): 142 | optimizer = Optimizer( 143 | method=method, 144 | problem=problem, 145 | round_no=round_no, 146 | init_var_value_array=init_var_value_array, 147 | var_value_arrays_to_tune=answers_to_tune, 148 | penalty_strength=penalty_strength_to_tune, 149 | debug_log_file_path=debug_log_file_path 150 | ) 151 | return optimizer.optimize(debug, debug_step_unit) 152 | -------------------------------------------------------------------------------- /codableopt/solver/optimizer/optimization_state.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Sequence 16 | import numpy as np 17 | 18 | from codableopt.solver.optimizer.entity.score_info import ScoreInfo 19 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 20 | 21 | 22 | class OptimizationState: 23 | 24 | def __init__(self, problem, init_var_value_array: np.array): 25 | self._problem = problem 26 | 27 | self._var_array = init_var_value_array.copy() 28 | self._best_var_array = self._var_array 29 | 30 | self._proposals = [] 31 | 32 | cashed_liner_constraint_sums = \ 33 | self._problem.constraints.liner_constraints.calc_constraint_sums(self._var_array) 34 | self._cashed_liner_constraint_sums = cashed_liner_constraint_sums 35 | self._penalty_coefficients = self._problem.constraints.init_penalty_coefficients.copy() 36 | # tune前は1に仮置き 37 | self._constraints_scale = np.array([1.0 for _ in self._penalty_coefficients]) 38 | self._exist_feasible_answer = False 39 | # tuning後に計算 40 | self._cashed_objective_score = None 41 | self._previous_score = None 42 | self._current_score = None 43 | self._best_score = None 44 | 45 | def init_scores(self): 46 | objective_score = self._problem.calc_objective(self._var_array, []) 47 | penalty_scores = self.calculate_penalties([]) 48 | penalty_score = sum(penalty_scores) 49 | if penalty_score == 0: 50 | self._exist_feasible_answer = True 51 | 52 | self._cashed_objective_score = objective_score 53 | self._previous_score = ScoreInfo(objective_score, penalty_score) 54 | self._current_score = ScoreInfo(objective_score, penalty_score) 55 | self._best_score = ScoreInfo(objective_score, penalty_score) 56 | 57 | def propose(self, proposals: Sequence[ProposalToMove]): 58 | objective_score = self._problem.calc_objective(self._var_array, 59 | proposals, 60 | self._cashed_objective_score) 61 | penalty_scores = self.calculate_penalties(proposals) 62 | penalty_score = sum(penalty_scores) 63 | 64 | self._proposals = proposals 65 | self._current_score.set_scores(objective_score, penalty_score) 66 | 67 | def apply_proposal(self): 68 | # 解を更新 69 | for proposal in self._proposals: 70 | self._var_array[proposal.var_index] = proposal.new_value 71 | 72 | # スコアを更新 73 | self._previous_score.set_scores(self._current_score.objective_score, 74 | self._current_score.penalty_score) 75 | 76 | # キャッシュを更新 77 | self._cashed_objective_score = self._current_score.objective_score 78 | self._problem.apply_proposal_to_liner_constraint_sums(self._proposals, 79 | self._cashed_liner_constraint_sums) 80 | 81 | is_best = False 82 | # 実行可能解がはじめて見つかった時 83 | if not self._exist_feasible_answer and self._current_score.penalty_score == 0: 84 | self._exist_feasible_answer = True 85 | is_best = True 86 | 87 | # 実行可能解ではないが、最適性が高まった時 88 | elif not self._exist_feasible_answer \ 89 | and self._current_score.score < self._best_score.score: 90 | is_best = True 91 | 92 | # 最適性が高まった時 93 | elif self._current_score.penalty_score == 0 \ 94 | and self._current_score.score < self._best_score.score: 95 | is_best = True 96 | 97 | # 最適解を更新 98 | if is_best: 99 | self._best_var_array = self._var_array.copy() 100 | self._best_score.set_scores(self._current_score.objective_score, 101 | self._current_score.penalty_score) 102 | 103 | def cancel_propose(self): 104 | # スコアを戻す 105 | self._current_score.set_scores(self._previous_score.objective_score, 106 | self._previous_score.penalty_score) 107 | 108 | def calculate_penalties(self, proposals): 109 | return self._problem.calc_penalties( 110 | self._var_array, proposals, self._penalty_coefficients, 111 | self._cashed_liner_constraint_sums) 112 | 113 | def decode(self, var_value_array: np.array): 114 | return self._problem.decode_answer(var_value_array) 115 | 116 | def tune_penalty(self, var_value_arrays, penalty_strength): 117 | objective_scores = \ 118 | [self._problem.calc_objective(var_array, []) for var_array in var_value_arrays] 119 | object_diff_score = np.double(max(objective_scores) - min(objective_scores)) 120 | 121 | vio_amounts_array = \ 122 | np.array([self._problem.calc_violation_amounts(var_array) for var_array in var_value_arrays]) 123 | 124 | if object_diff_score > 0: 125 | vio_means = vio_amounts_array.mean(axis=0) 126 | self._constraints_scale = [1.0 if x == 0 else x for x in vio_means] 127 | self._penalty_coefficients = \ 128 | [1.0 if x == 0 else object_diff_score / x for x in vio_means] 129 | self._penalty_coefficients = [x * penalty_strength for x in self._penalty_coefficients] 130 | 131 | @property 132 | def problem(self): 133 | return self._problem 134 | 135 | @property 136 | def var_array(self): 137 | return self._var_array 138 | 139 | @property 140 | def best_var_array(self): 141 | return self._best_var_array 142 | 143 | @property 144 | def previous_score(self): 145 | return self._previous_score 146 | 147 | @property 148 | def current_score(self): 149 | return self._current_score 150 | 151 | @property 152 | def best_score(self): 153 | return self._best_score 154 | 155 | @property 156 | def cashed_objective_score(self): 157 | return self._cashed_objective_score 158 | 159 | @property 160 | def cashed_liner_constraint_sums(self): 161 | return self._cashed_liner_constraint_sums 162 | 163 | @property 164 | def exist_feasible_answer(self): 165 | return self._exist_feasible_answer 166 | 167 | @property 168 | def penalty_coefficients(self): 169 | return self._penalty_coefficients 170 | 171 | @penalty_coefficients.setter 172 | def penalty_coefficients(self, penalty_coefficients): 173 | self._penalty_coefficients = penalty_coefficients 174 | 175 | @property 176 | def constraints_scale(self): 177 | return self._constraints_scale 178 | 179 | @constraints_scale.setter 180 | def constraints_scale(self, constraints_scale): 181 | self._constraints_scale = constraints_scale 182 | -------------------------------------------------------------------------------- /codableopt/solver/optimizer/optimizer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from pathlib import Path 16 | from typing import Optional 17 | from logging import getLogger, StreamHandler, FileHandler, INFO 18 | from time import time 19 | 20 | import numpy as np 21 | 22 | from codableopt.solver.formulation.solver_problem import SolverProblem 23 | from codableopt.solver.optimizer.method.optimizer_method import OptimizerMethod 24 | from codableopt.solver.optimizer.optimization_state import OptimizationState 25 | 26 | 27 | class Optimizer: 28 | """最適化を行うクラス。手法は、methodに従う。 29 | """ 30 | 31 | def __init__( 32 | self, 33 | problem: SolverProblem, 34 | method: OptimizerMethod, 35 | round_no: int, 36 | init_var_value_array: np.array, 37 | var_value_arrays_to_tune: Optional, 38 | penalty_strength, 39 | debug_log_file_path: Optional[Path] = None): 40 | self._logger = getLogger(__name__) 41 | self._logger.setLevel(INFO) 42 | 43 | self._state = OptimizationState(problem, init_var_value_array) 44 | if var_value_arrays_to_tune is not None: 45 | self._state.tune_penalty(var_value_arrays_to_tune, np.double(penalty_strength)) 46 | self._state.init_scores() 47 | 48 | self._problem = problem 49 | self._method = method 50 | self._round_no = round_no 51 | 52 | if len(self._logger.handlers) == 0: 53 | self._logger.addHandler(StreamHandler()) 54 | if debug_log_file_path is not None: 55 | self._logger.addHandler(FileHandler(filename=debug_log_file_path)) 56 | 57 | def optimize(self, debug: bool = False, debug_unit_step: int = 100) -> OptimizationState: 58 | """最適化を行う関数。 59 | 60 | Args: 61 | debug: デバッグフラッグ 62 | debug_unit_step: デバッグ情報を表示するステップ間隔 63 | 64 | Returns: 65 | 最適解, 最適解のスコア 66 | """ 67 | if debug: 68 | self._logger.info(f'log_type:method,round:{self._round_no},' 69 | f'method_name:{self._method.name()}') 70 | 71 | start_time = time() 72 | 73 | step = 0 74 | move_cnt = 0 75 | 76 | while step < self._method.steps: 77 | step += 1 78 | self._method.initialize_of_step(self._state, step) 79 | 80 | proposed_move_list = self._method.propose(self._state, step) 81 | self._state.propose(proposed_move_list) 82 | 83 | move_flg = self._method.judge(self._state, step) 84 | 85 | if move_flg: 86 | move_cnt += 1 87 | self._state.apply_proposal() 88 | 89 | self._method.finalize_of_step(self._state, step) 90 | 91 | if not move_flg: 92 | self._state.cancel_propose() 93 | 94 | if debug and step % debug_unit_step == 0: 95 | self._logger.info( 96 | f'log_type:optimize,' 97 | f'round:{self._round_no},' 98 | f'step:{step},' 99 | f'move_count:{move_cnt}/{debug_unit_step},' 100 | f'best_score:{self._state.best_score.score},' 101 | f'best_is_feasible:{self._state.exist_feasible_answer},' 102 | f'score:{self._state.current_score.score},' 103 | f'objective_score:{self._state.current_score.objective_score},' 104 | f'penalty_score:{self._state.current_score.penalty_score}') 105 | move_cnt = 0 106 | 107 | end_time = time() 108 | if debug: 109 | self._logger.info( 110 | f'log_type:time,' 111 | f'round:{self._round_no},' 112 | f'step:{step},' 113 | f'calculation_time:{end_time - start_time},' 114 | f'best_score:{self._state.best_score.score},' 115 | f'best_is_feasible:{self._state.exist_feasible_answer}') 116 | 117 | return self._state 118 | 119 | @property 120 | def problem(self) -> SolverProblem: 121 | return self._problem 122 | -------------------------------------------------------------------------------- /codableopt/solver/sampler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/codableopt/solver/sampler/__init__.py -------------------------------------------------------------------------------- /codableopt/solver/sampler/var_value_array_sampler.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List 16 | import numpy as np 17 | 18 | from codableopt.solver.formulation.solver_problem import SolverProblem 19 | 20 | 21 | class VarValueArraySampler: 22 | """ランダムに解答を生成するサンプラークラス。 23 | """ 24 | 25 | def __init__(self): 26 | """ランダムに解答を生成するサンプラーのオブジェクト生成関数。 27 | """ 28 | pass 29 | 30 | @staticmethod 31 | def sample( 32 | solver_problem: SolverProblem, 33 | generate_num: int, 34 | choice_num: int): 35 | """ランダムに解答を生成し、正規化した特徴量の値からユーグリッド距離を基準として、 36 | 解答同士が距離が最大になる組み合わせの解答を選択する関数。 37 | 38 | Args: 39 | solver_problem (SolverProblem): ソルバー用に変換した最適化問題 40 | generate_num (int): 解答を生成する数 41 | choice_num (int): 生成した解答から選択する数 42 | Returns: 43 | サンプリングした解答のリスト 44 | """ 45 | answers = VarValueArraySampler.generate(solver_problem, generate_num) 46 | return VarValueArraySampler.choice(answers, num=choice_num) 47 | 48 | @staticmethod 49 | def generate(solver_problem: SolverProblem, generate_num: int) -> np.ndarray: 50 | """ランダムに解答を生成する関数。 51 | 52 | Args: 53 | solver_problem (SolverProblem): ソルバー用に変換した最適化問題 54 | generate_num (int): 解答を生成する数 55 | 56 | Returns: 57 | 生成したランダムな解答ロスト 58 | """ 59 | 60 | return np.array([VarValueArraySampler._generate_random_var_value_array(solver_problem) 61 | for _ in range(generate_num)]) 62 | 63 | @staticmethod 64 | def _generate_random_var_value_array(solver_problem: SolverProblem) -> np.ndarray: 65 | """ランダムに解答を生成する関数。 66 | 67 | Args: 68 | solver_problem (SolverProblem): ソルバー用に変換した最適化問題 69 | 70 | Returns: 71 | 生成したランダムな解答 72 | """ 73 | values = [] 74 | for variable in solver_problem.variables: 75 | values.extend(variable.random_values()) 76 | 77 | return np.array(values) 78 | 79 | @staticmethod 80 | def choice(answers: np.ndarray, num: int) -> List[np.ndarray]: 81 | """引数の解答から、正規化した特徴量の値からユーグリッド距離を基準として、 82 | 解答同士が距離が最大になる組み合わせの解答を選択する関数。 83 | 84 | Args: 85 | answers: 選択元の解答群 86 | num: 選択する解答数 87 | 88 | Returns: 89 | 選択した解答 90 | """ 91 | # 初期解の各変数の正規化を行う 92 | min_answers = np.min(answers, axis=0) 93 | max_answers = np.max(answers, axis=0) 94 | normalized_sample_answers = \ 95 | np.nan_to_num((answers - min_answers) / (max_answers - min_answers), 96 | nan=1, posinf=1, neginf=1) 97 | 98 | # 最初に生成した解を最初の初期解として加える 99 | init_answers = [answers[0, :]] 100 | normalized_init_answers = [normalized_sample_answers[0, :]] 101 | answers = np.delete(answers, 0, axis=0) 102 | normalized_sample_answers = np.delete(normalized_sample_answers, 0, axis=0) 103 | 104 | # 選択している初期解の中との最初距離が最大の初期解候補を加えていく 105 | for _ in range(num - 1): 106 | selected_index_no, max_distance = None, None 107 | for index_no, (sample_answer, normalized_sample_answer)\ 108 | in enumerate(zip(answers, normalized_sample_answers)): 109 | distance = VarValueArraySampler.__calculate_min_distance_from_answers( 110 | normalized_sample_answer, normalized_init_answers) 111 | if max_distance is None or max_distance < distance: 112 | max_distance = distance 113 | selected_index_no = index_no 114 | 115 | init_answers.append(answers[selected_index_no, :]) 116 | normalized_init_answers.append(normalized_sample_answers[selected_index_no, :]) 117 | answers = np.delete(answers, selected_index_no, axis=0) 118 | normalized_sample_answers = \ 119 | np.delete(normalized_sample_answers, selected_index_no, axis=0) 120 | 121 | return init_answers 122 | 123 | @staticmethod 124 | def __calculate_min_distance_from_answers(answer, base_answers): 125 | """選択済みの解答群の全ての解答に対して、選択候補の解答とのユーグリッド距離を計算し、 126 | その中の最短距離を計算する関数。 127 | 128 | Args: 129 | answer: 選択候補の解答 130 | base_answers: 選択済みの解答群 131 | 132 | Returns: 133 | 最短距離となるユーグリッド距離 134 | """ 135 | return min([np.linalg.norm(answer - base_answer) for base_answer in base_answers]) 136 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | import sys 15 | import os 16 | 17 | sys.path.insert(0, os.path.abspath('../')) 18 | sys.path.insert(0, os.path.abspath('../codableopt')) 19 | # from solver.opt_solver import OptSolver 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'codableopt' 24 | copyright = '2022, tomomoto' 25 | author = 'tomomoto' 26 | 27 | # The short X.Y version 28 | version = 'v0.1' 29 | 30 | # The full version, including alpha/beta/rc tags 31 | release = 'v0.1' 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.viewcode', 42 | 'sphinx.ext.todo', 43 | 'sphinx.ext.napoleon', 44 | "sphinx_autodoc_typehints" 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The language for content autogenerated by Sphinx. Refer to documentation 51 | # for a list of supported languages. 52 | # 53 | # This is also used if you do content translation via gettext catalogs. 54 | # Usually you set "language" from the command line for these cases. 55 | language = 'en' 56 | 57 | # List of patterns, relative to source directory, that match files and 58 | # directories to ignore when looking for source files. 59 | # This pattern also affects html_static_path and html_extra_path. 60 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 61 | 62 | 63 | # -- Options for HTML output ------------------------------------------------- 64 | 65 | # The theme to use for HTML and HTML Help pages. See the documentation for 66 | # a list of builtin themes. 67 | # 68 | html_theme = 'sphinx_rtd_theme' 69 | 70 | # Add any paths that contain custom static files (such as style sheets) here, 71 | # relative to this directory. They are copied after the builtin static files, 72 | # so a file named "default.css" will overwrite the builtin "default.css". 73 | html_static_path = ['_static'] 74 | 75 | 76 | # -- Extension configuration ------------------------------------------------- 77 | 78 | # -- Options for todo extension ---------------------------------------------- 79 | 80 | # If true, `todo` and `todoList` produce output, else they produce nothing. 81 | todo_include_todos = True 82 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. include:: ../README.rst 3 | :start-after: index-start-installation-marker 4 | :end-before: index-end-installation-marker 5 | 6 | Sample: 7 | ============ 8 | 9 | .. include:: ../README.rst 10 | :start-after: index-start-sample1 11 | :end-before: index-end-sample1 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | :caption: Manual: 17 | 18 | ./manual/getting_started 19 | ./manual/advanced_usage 20 | ./manual/algorithm 21 | 22 | .. toctree:: 23 | :maxdepth: 1 24 | :caption: Reference: 25 | 26 | ./reference/class_reference 27 | 28 | .. toctree:: 29 | :maxdepth: 1 30 | :caption: Sample Codes: 31 | 32 | ./sample/sample_codes 33 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/manual/advanced_usage.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Advanced Usage 3 | ===================== 4 | 5 | Delta Objective Function 6 | =============================================== 7 | 8 | 目的関数の計算は、関数によっては非常に計算コストが高くなります。しかし、差分計算を用いることで目的関数の計算コストを下げることができます。本ソルバーでも、目的関数の差分計算関数を設定することができます。差分計算は、Objectiveオブジェクト生成時の引数にdelta_objectiveを設定することで利用できます。なお、差分計算関数の引数には、目的関数と同様の引数に加えて、遷移前の変数の値が元の変数名の前にpre_をつけた名前で渡されます。 9 | 10 | .. code-block:: python 11 | 12 | x = IntVariable(name='x', lower=np.double(0.0), upper=None) 13 | y = DoubleVariable(name='y', lower=np.double(0.0), upper=None) 14 | 15 | # 目的関数 16 | def obj_fun(var_x, var_y): 17 | return 3 * var_x + 5 * var_y 18 | 19 | # 目的関数の差分計算用の関数 20 | def delta_obj_fun(pre_var_x, pre_var_y, var_x, var_y, parameters): 21 | delta_value = 0 22 | if pre_var_x != var_x: 23 | delta_value += parameters['coef_x'] * (var_x - pre_var_x) 24 | if pre_var_y != var_y: 25 | delta_value += parameters['coef_y'] * (var_y - pre_var_y) 26 | return delta_value 27 | 28 | # 目的関数を定義 29 | problem += Objective(objective=obj_fun, 30 | delta_objective=delta_obj_fun, 31 | args_map={'var_x': x, 'var_y': y, 32 | 'parameters': {'coef_x': 3.0, 'coef_y': 2.0}}) 33 | 34 | Custom Optimization Method 35 | ============================== 36 | 37 | 本ソルバーは、共通アルゴリズム上で最適化手法をカスタマイズして利用することはできます。最適化手法をカスタマイズする場合は、本ソルバーが提供しているOptimizerMethodを継承して実装することで実現することができます。本ソルバーが提供しているペナルティ係数調整手法もその枠組み上で実装されています。 38 | 39 | OptimizerMethod 40 | ----------------- 41 | 42 | .. autoclass:: codableopt.solver.optimizer.method.optimizer_method.OptimizerMethod 43 | :members: 44 | 45 | Sample Code 46 | ----------------- 47 | 48 | .. code-block:: python 49 | 50 | from typing import List 51 | from random import choice 52 | 53 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 54 | from codableopt.solver.optimizer.method.optimizer_method import OptimizerMethod 55 | from codableopt.solver.optimizer.optimization_state import OptimizationState 56 | 57 | 58 | class SampleMethod(OptimizerMethod): 59 | 60 | def __init__(self, steps: int): 61 | super().__init__(steps) 62 | 63 | def name(self) -> str: 64 | return 'sample_method' 65 | 66 | def initialize_of_step(self, state: OptimizationState, step: int): 67 | # ステップ開始時の処理なし 68 | pass 69 | 70 | def propose(self, state: OptimizationState, step: int) -> List[ProposalToMove]: 71 | # 変数から1つランダムに選択する 72 | solver_variable = choice(state.problem.variables) 73 | # 選択した変数をランダムに移動する解の遷移を提案する 74 | return solver_variable.propose_random_move(state) 75 | 76 | def judge(self, state: OptimizationState, step: int) -> bool: 77 | # 遷移前と遷移後のスコアを比較 78 | delta_energy = state.current_score.score - state.previous_score.score 79 | # ソルバー内はエネルギーが低い方が最適性が高いことを表している 80 | # マイナスの場合に解が改善しているため、提案を受け入れる 81 | return delta_energy < 0 82 | 83 | def finalize_of_step(self, state: OptimizationState, step: int): 84 | # ステップ終了時の処理なし 85 | pass 86 | 87 | 88 | [deprecation] User Define Constraint 89 | =============================================== 90 | 91 | 非推奨ではありますが、本ソルバーでは、制約式を関数として渡すこともできます。関数の返り値には制約違反量を設定します。引数は、目的関数同様にargs_mapを設定することで指定できます。ただし、デフォルトで提供しているmethodでは、User Define Constraintを利用している最適化問題は実用に耐えうる最適化精度を実現できません。そのため現状では利用することは推奨していません。 92 | 93 | Sample Code 94 | ----------------- 95 | 96 | .. code-block:: python 97 | 98 | # 制約式を定義 99 | def udf_constraint_function(var_x, var_y, var_z): 100 | violation_amount = 2 * var_x + 4 * var_y - 8 101 | if var_z == 'a': 102 | violation_amount += 2 103 | else: 104 | violation_amount += 3 105 | 106 | if violation_amount <= 0: 107 | return 0 108 | else: 109 | return violation_amount 110 | 111 | 112 | constant = UserDefineConstraint(udf_constraint_function, 113 | args_map={'var_x': x, 'var_y': y, 'var_z': z}, 114 | constraint_name='user_define_constraint') 115 | problem += constant 116 | -------------------------------------------------------------------------------- /docs/manual/algorithm.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Algorithm 3 | ===================== 4 | 5 | 本ソルバーの最適化アルゴリズムは、「全手法で共通のアルゴリズム部分」と「各手法で異なるアルゴリズム部分」に分かれています。「全手法で共通のアルゴリズム部分」はカスタマイズできませんが、「各手法で異なるアルゴリズム部分」はカスタムマイズすることができます。また、本ソルバーでは、ペナルティ係数調整手法という名称で最適化手法を提供しています。 6 | 7 | Common Algorithm 8 | ===================== 9 | 10 | Algorithm of OptSolver 11 | -------------------------- 12 | 13 | 共通アルゴリズムは、下記のようなステップのアルゴリズムです。 14 | 15 | 1. **初期解の生成** 16 | 17 | num_to_select_init_answerで指定した数、ランダムな解を生成します。ランダムな解は、変数毎に上界/下界またはカテゴリ値からランダムに選択した値です。生成したランダムな解を各変数のスケールを正規化し、簡易的なアルゴリズムによって選択した解群間のユーグリッド距離の合計が最大となるようなround_times個の解群を初期解群として採用します。ただし、引数のinit_answersに初期解が設定されている場合は、ランダムな解を生成せずに、指定された初期解を利用します。 18 | 19 | 2. **最適化の実行** 20 | 21 | 採用した初期解毎に最適化の実行します。また、このときn_jobs引数によって並列処理の実行を設定している場合は、初期解毎に並列処理が実行されます。全ての初期解に対する最適化が完了したら、返された最適化処理の結果から実行可能解がある場合はその中から最も目的関数の値が最も良い解を、実行可能解がない場合はその中から目的関数の値に制約違反ペナルティを加えた値が最も良い解を選択して、最適化の実行結果として返します。(制約違反ペナルティのペナルティ係数は、共通ではなく、各最適化処理内で決定している値を利用する。結果、フェアな比較ではないが、実行可能解がない場合の解は参考程度の解なので、現状このような仕様となっている。また今後、細かな結果など含めて最適化結果を返せるようなインタフェースを検討する予定です。。) 22 | 23 | 24 | Algorithm of Optimizer 25 | -------------------------- 26 | 27 | 1. **ペナルティ係数の調整** 28 | 29 | 初期解(最適化試行)毎に、answer_num_to_tune_penaltyで指定した数、ランダムな解を生成します。ランダムな解は、変数毎に上界/下界またはカテゴリ値からランダムに選択した値とする。生成したランダムな解群から各制約式の違反量の違反量を計算し、「生成したランダムな解群における目的関数の最大値と最小値の差分のpenalty_strength倍」と「各制約式のペナルティスコア」が等しくなるような各制約式のペナルティ係数に調整します。 30 | 31 | 2. **初期解のスコア計算** 32 | 33 | 初期解から目的関数の値と制約違反のペナルティの値を計算し、合算して初期解のスコアとして採用します。また、初期解を現状の最適解として採択します。 34 | 35 | 3. **method.initialize_of_step実施** 36 | 37 | 設定したmethodのinitialize_of_step関数を呼び出します。 38 | 39 | 4. **method.propose実施** 40 | 41 | 設定したmethodのpropose関数を呼び出し、解の遷移案を取得します。 42 | 43 | 5. **提案された解のスコア計算** 44 | 45 | 解の遷移案を実行した場合のスコア(目的関数の値と制約違反のペナルティの値の合算)を計算します。 46 | 47 | 6. **method.judge実施** 48 | 49 | 設定したmethodのjudge関数を呼び出し、解の遷移を実施有無を決定します。解の遷移が決定した場合は、現状の解を遷移させる。 50 | 51 | 7. **最適性確認** 52 | 53 | 6で解が遷移した場合は、新たな解の最適性の確認を行います。最適解は、実行可能解を優先し、その上でスコアが良い方を優先するように選択します。現状の最適解より良い場合は、最適解を変更します。 54 | 55 | 8. **method.finalize_of_step実施** 56 | 57 | 設定したmethodのfinalize_of_step実施関数を呼び出します。 58 | 59 | 9. **終了判定** 60 | 61 | 3-8を繰り返した回数を計算する。methodで設定したsteps数に達した場合は、終了とし、現状の最適解を返す。達していない場合は、3に戻り、同様に処理を繰り返していきます。 62 | 63 | 64 | Method 65 | ===================== 66 | 67 | Penalty Adjustment Method 68 | ------------------------------ 69 | 70 | 1. **method.initialize_of_step** 71 | 72 | ステップ開始時の処理はありません。 73 | 74 | 2. **method.propose** 75 | 76 | random_movement_rateの確率で、ランダム遷移を提案する。また、(1-random_movement_rate)の確率で、ペナルティが小さくなる遷移を提案します。 77 | ランダム遷移を提案する時は、最適化問題内の1つの変数をランダムに選択し、上界から下界値またはカテゴリ値から1つの値を選択し、提案します。ただし、変数が数値かつデータ遷移履歴がhistory_value_size以上のデータ件数の場合は、対象変数のデータ遷移履歴値の平均値と標準偏差値を計算し、「平均値 - 標準偏差値 * range_std_rate」から「平均値 + 標準偏差値 * range_std_rate」までの値の範囲からランダムで値を選択して、提案します。またペナルティが小さくなる遷移を提案する時は、最適化問題内の1つの変数をランダムに選択し、その変数を動かすことでペナルティが減るような値を計算によって求め、提案します。また、ペナルティが最小になる複数の値が存在した場合は、その中からランダムに選択し、提案します。(*この計算において、UserDefineConstraintは対象になりません。) 78 | 79 | 3. **method.judge** 80 | 81 | スコアを比較し、現状の解より良い場合は遷移するという判定結果を、それ以外の場合は遷移しないという判定結果を返します。 82 | 83 | 4. **method.finalize_of_step** 84 | 85 | 「最後に解が遷移してから経過したStep数」と「最後にペナルティ係数を更新してから経過したStep数」を計算し、小さな方の値を採用し、その値がsteps_while_not_improve_score以上に達していたらペナルティ係数を調整します。 86 | 現状の解が実行可能解である場合は、ペナルティ係数に「現状の解のペナルティ違反量を正規化した値」と(1 + self._delta_penalty_rate)を積算し、ペナルティ係数をあげます。また、現状の解が実行可能解ではない場合は、一律にペナルティ係数に(1 - delta_penalty_rate)を積算し、ペナルティ係数を下げます。 87 | -------------------------------------------------------------------------------- /docs/manual/getting_started.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Getting Started 3 | ===================== 4 | 5 | .. include:: ../../README.rst 6 | :start-after: index-start-installation-marker 7 | :end-before: index-end-installation-marker 8 | 9 | Basic Usage 10 | ============ 11 | 12 | 1. **問題を設定** 13 | 14 | 問題オブジェクトを生成する際に、最大化または最小化問題のどちらかを指定をする必要があります。is_max_problemが、Trueの場合は最大化問題、Falseの場合は最小化問題となります。 15 | 16 | >>> from codableopt import Problem 17 | >>> problem = Problem(is_max_problem=True) 18 | 19 | 20 | 2. **変数を定義** 21 | 22 | 利用する変数を定義します。生成した変数オブジェクトは、制約式や目的関数の引数に利用することができます。 23 | 24 | >>> from codableopt import IntVariable, DoubleVariable, CategoryVariable 25 | >>> x = IntVariable(name='x', lower=np.double(0), upper=np.double(5)) 26 | >>> y = DoubleVariable(name='y', lower=np.double(0.0), upper=None) 27 | >>> z = CategoryVariable(name='z', categories=['a', 'b', 'c']) 28 | 29 | 変数は、内包表記やfor文によってまとめて定義することもできます。 30 | 31 | >>> from codableopt import IntVariable 32 | >>> x = [IntVariable(name=f'x_{no}', lower=None, upper=None) for no in range(100)] 33 | 34 | 35 | 3. **目的関数を設定** 36 | 37 | 目的関数を問題に設定します。目的関数は、Objectiveオブジェクトを問題オブジェクトに加えることによって、設定できます。Objectiveオブジェクトを生成時には、「目的関数を計算するPython関数」と「引数のマッピング情報」を引数に設定します。 38 | 「引数のマッピング情報」は、Dict型で設定し、keyは目的関数の引数名、valueは変数オブジェクトまたは定数やPythonオブジェクトなどを指定します。なお、引数にマッピングした変数オブジェクトは、目的関数を計算するPython関数内では、最適化計算中の変数の値に変換されてから、引数に渡されます。 39 | 40 | >>> def objective_function(var_x, var_y, var_z, parameters): 41 | >>> obj_value = parameters['coef_x'] * var_x + parameters['coef_y'] * var_y 42 | >>> if var_z == 'a': 43 | >>> obj_value += 10.0 44 | >>> elif var_z == 'b': 45 | >>> obj_value += 8.0 46 | >>> else: 47 | >>> # var_z == 'c' 48 | >>> obj_value -= 3.0 49 | >>> 50 | >>> return obj_value 51 | >>> 52 | >>> problem += Objective(objective=objective_function, 53 | >>> args_map={'var_x': x, 'var_y': y, 'var_z': z, 54 | >>> 'parameters': {'coef_x': -3.0, 'coef_y': 4.0}}) 55 | 56 | 「引数のマッピング情報」には、変数リストを渡すこともできます。 57 | 58 | >>> from codableopt import IntVariable 59 | >>> x = [IntVariable(name=f'x_{no}', lower=None, upper=None) for no in range(100)] 60 | >>> 61 | >>> problem += Objective(objective=objective_function, args_map={'var_x': x}}) 62 | 63 | 64 | 4. **制約式を定義** 65 | 66 | 制約式を問題に設定します。制約は、制約式オブジェクトを問題オブジェクトに加えることによって、設定できます。制約式オブジェクトは、変数オブジェクトと不等式を組み合わせることによって生成できます。不等式には、<,<=,>,>=,==が利用できます。また、1次式の制約式しか利用できません。 67 | 68 | >>> constant = 2 * x + 4 * y + 2 * (z == 'a') + 3 * (z == ('b', 'c')) <= 8 69 | >>> problem += constant 70 | 71 | 5. **最適化計算を実行** 72 | 73 | ソルバーオブジェクトと最適化手法オブジェクトを生成し、ソルバーオブジェクトに問題オブジェクトと最適化手法オブジェクトを渡し、最適化計算を行います。ソルバーは、得られた最も良い解と得られた解が制約を全て満たすかの判定フラグを返します。 74 | 75 | >>> solver = OptSolver(round_times=2) 76 | >>> method = PenaltyAdjustmentMethod(steps=40000) 77 | >>> answer, is_feasible = solver.solve(problem, method) 78 | >>> print(f'answer:{answer}') 79 | answer:{'x': 0, 'y': 1.5, 'z': 'a'} 80 | >>> print(f'answer_is_feasible:{is_feasible}') 81 | answer_is_feasible:True 82 | 83 | Variable 84 | ============ 85 | 86 | 整数・連続値・カテゴリの3種類の変数を提供しています。各変数は、目的関数に渡す引数や制約式に利用します。どの種類の変数も共通で、変数名を設定することができます。変数名は、最適化の解を返す際に利用されます。 87 | 88 | IntVariable 89 | -------------- 90 | 91 | IntVariableは、整数型の変数です。lowerには下界値、upperには上界値を設定します。なお、境界値は可能な値として設定されます。また、Noneを設定した場合は、下界値/上界値が設定されます。IntVariableは、目的関数に渡す引数と制約式のどちらにも利用できます。 92 | 93 | .. code-block:: python 94 | 95 | from codableopt import IntVariable 96 | x = IntVariable(name='x', lower=0, upper=None) 97 | 98 | DoubleVariable 99 | ------------------ 100 | 101 | DoubleVariableは、連続値型の変数です。lowerには下界値、upperには上界値を設定します。なお、境界値は可能な値として設定されます。また、Noneを設定した場合は、下界値/上界値が設定されます。DoubleVariableは、目的関数に渡す引数と制約式のどちらにも利用できます。 102 | 103 | .. code-block:: python 104 | 105 | from codableopt import DoubleVariable 106 | x = DoubleVariable(name='x', lower=None, upper=2.3) 107 | 108 | CategoryVariable 109 | ------------------- 110 | 111 | CategoryVariableは、カテゴリ型の変数です。categoriesには、取り得るカテゴリ値を設定します。CategoryVariableは、目的関数に渡すことはできるが、制約式に利用することはできません。カテゴリ値を制約式に利用したい場合は、CategoryCaseVariableを利用する必要があります。 112 | 113 | .. code-block:: python 114 | 115 | from codableopt import CategoryVariable 116 | x = CategoryVariable(name='x', categories=['a', 'b', 'c']) 117 | 118 | 119 | CategoryCaseVariableは、カテゴリ型の変数と等式を組み合わせることで生成できます。Tupleを利用すれば、比較する値を複数設定でき、いずれかと等しい場合は1、それ以外の場合は0となります。CategoryCaseVariableは、目的関数に渡す引数と制約式のどちらにも利用できます。 120 | 121 | .. code-block:: python 122 | 123 | # xが'a'の時は1、'b'または'c'の時は0となるCategoryCaseVariable 124 | x_a = x == 'a' 125 | # xがb'または'c'の時は1、'a'の時は0となるCategoryCaseVariable 126 | x_bc = x == ('b', 'c') 127 | -------------------------------------------------------------------------------- /docs/reference/class_reference.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Class Reference 3 | ==================== 4 | 5 | .. currentmodule:: codableopt 6 | 7 | OptSolver 8 | ========== 9 | 10 | .. autoclass:: OptSolver 11 | :special-members: __init__ 12 | :members: 13 | 14 | Method 15 | ========== 16 | 17 | .. autoclass:: PenaltyAdjustmentMethod 18 | :special-members: __init__ 19 | 20 | Problem 21 | ========== 22 | 23 | .. autoclass:: Problem 24 | :special-members: __init__ 25 | :members: 26 | 27 | Objective 28 | ========== 29 | 30 | .. autoclass:: Objective 31 | :special-members: __init__ 32 | :members: 33 | 34 | IntVariable 35 | ==================== 36 | 37 | .. autoclass:: IntVariable 38 | :special-members: __init__ 39 | :members: 40 | 41 | DoubleVariable 42 | ==================== 43 | 44 | .. autoclass:: DoubleVariable 45 | :special-members: __init__ 46 | :members: 47 | 48 | CategoryVariable 49 | ==================== 50 | 51 | .. autoclass:: CategoryVariable 52 | :special-members: __init__ 53 | :members: 54 | -------------------------------------------------------------------------------- /docs/sample/sample_codes.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Sample Codes 3 | ================== 4 | 5 | - `基本サンプル `_ 6 | - `Use Delta Objective Functionのサンプル `_ 7 | - `ナップサップ問題 `_ 8 | - `TSP問題 `_ 9 | - `時間帯別距離考慮のTSP問題(if文を利用した目的関数のケース) `_ 10 | - `CM最適化問題(複雑な計算が必要な目的関数のケース) `_ 11 | - `クーポン最適化問題(機械学習モデルを利用した目的関数のケース) `_ 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | 'wheel>=0.36.2', 4 | "setuptools" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements_doc.txt: -------------------------------------------------------------------------------- 1 | Sphinx>=3.5.4 2 | sphinx-rtd-theme>=0.5.2 3 | sphinx-autodoc-typehints>=1.17.0 -------------------------------------------------------------------------------- /sample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/sample/__init__.py -------------------------------------------------------------------------------- /sample/method/random_move_method.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List 16 | from random import choice 17 | 18 | from codableopt.solver.optimizer.entity.proposal_to_move import ProposalToMove 19 | from codableopt.solver.optimizer.method.optimizer_method import OptimizerMethod 20 | from codableopt.solver.optimizer.optimization_state import OptimizationState 21 | 22 | 23 | class SampleMethod(OptimizerMethod): 24 | 25 | def __init__(self, steps: int): 26 | super().__init__(steps) 27 | 28 | def name(self) -> str: 29 | return 'sample_method' 30 | 31 | def initialize_of_step(self, state: OptimizationState, step: int): 32 | # ステップ開始時の処理なし 33 | pass 34 | 35 | def propose(self, state: OptimizationState, step: int) -> List[ProposalToMove]: 36 | # 変数から1つランダムに選択する 37 | solver_variable = choice(state.problem.variables) 38 | # 選択した変数をランダムに移動する解の遷移を提案する 39 | return solver_variable.propose_random_move(state) 40 | 41 | def judge(self, state: OptimizationState, step: int) -> bool: 42 | # 遷移前と遷移後のスコアを比較 43 | delta_energy = state.current_score.score - state.previous_score.score 44 | # ソルバー内はエネルギーが低い方が最適性が高いことを表している 45 | # マイナスの場合に解が改善しているため、提案を受け入れる 46 | return delta_energy < 0 47 | 48 | def finalize_of_step(self, state: OptimizationState, step: int): 49 | # ステップ終了時の処理なし 50 | pass 51 | -------------------------------------------------------------------------------- /sample/usage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/sample/usage/__init__.py -------------------------------------------------------------------------------- /sample/usage/problem/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/sample/usage/problem/__init__.py -------------------------------------------------------------------------------- /sample/usage/problem/matching_problem_generator.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List 16 | from dataclasses import dataclass 17 | import random 18 | 19 | import numpy as np 20 | import pandas as pd 21 | from sklearn import tree 22 | 23 | 24 | ATTRIBUTE_PATTERN_NUM = 10 25 | 26 | 27 | @dataclass(frozen=True) 28 | class Customer: 29 | ATTRIBUTE_A_MASTER = list(range(ATTRIBUTE_PATTERN_NUM)) 30 | ATTRIBUTE_B_MASTER = list(range(ATTRIBUTE_PATTERN_NUM)) 31 | ATTRIBUTE_C_MASTER = list(range(ATTRIBUTE_PATTERN_NUM)) 32 | name: str 33 | attribute_a: int 34 | attribute_b: int 35 | attribute_c: int 36 | 37 | 38 | @dataclass(frozen=True) 39 | class Item: 40 | ATTRIBUTE_A_MASTER = list(range(ATTRIBUTE_PATTERN_NUM)) 41 | ATTRIBUTE_B_MASTER = list(range(ATTRIBUTE_PATTERN_NUM)) 42 | ATTRIBUTE_C_MASTER = list(range(ATTRIBUTE_PATTERN_NUM)) 43 | name: str 44 | price: int 45 | cost: int 46 | attribute_a: int 47 | attribute_b: int 48 | attribute_c: int 49 | 50 | 51 | @dataclass(frozen=True) 52 | class Coupon: 53 | DOWN_PRICE_TYPE_MASTER = [0, 1000, 3000, 5000] 54 | name: str 55 | down_price: int 56 | 57 | 58 | class DataGenerator: 59 | 60 | def __init__(self): 61 | self._attribute_matching_scores = \ 62 | np.random.randint(0, 2000, (ATTRIBUTE_PATTERN_NUM, ATTRIBUTE_PATTERN_NUM)) 63 | 64 | self._customer_attribute_a_scores = np.random.randint(0, 2000, ATTRIBUTE_PATTERN_NUM) 65 | self._customer_attribute_b_scores = np.random.randint(0, 2000, ATTRIBUTE_PATTERN_NUM) 66 | self._customer_attribute_c_scores = np.random.randint(0, 2000, ATTRIBUTE_PATTERN_NUM) 67 | 68 | self._item_attribute_a_scores = np.random.randint(0, 2000, ATTRIBUTE_PATTERN_NUM) 69 | self._item_attribute_b_scores = np.random.randint(0, 2000, ATTRIBUTE_PATTERN_NUM) 70 | self._item_attribute_c_scores = np.random.randint(0, 2000, ATTRIBUTE_PATTERN_NUM) 71 | 72 | def generate_train_dataset(self, data_num: int) -> pd.DataFrame: 73 | dataset = [] 74 | all_coupons = self.generate_all_coupons() 75 | for no in range(data_num): 76 | customer = self.generate_random_customer(f'customer_{no}') 77 | item = self.generate_random_item(f'item_{no}') 78 | coupon = random.choice(all_coupons) 79 | buy_int_flag = self._simulate_buy_flg(customer, item, coupon) 80 | dataset.append([customer.attribute_a, 81 | customer.attribute_b, 82 | customer.attribute_c, 83 | item.price, 84 | item.attribute_a, 85 | item.attribute_b, 86 | item.attribute_c, 87 | coupon.down_price, 88 | buy_int_flag]) 89 | return pd.DataFrame( 90 | dataset, 91 | columns=[ 92 | 'customer_attribute_a', 93 | 'customer_attribute_b', 94 | 'customer_attribute_c', 95 | 'item_price', 96 | 'item_attribute_a', 97 | 'item_attribute_b', 98 | 'item_attribute_c', 99 | 'coupon_down_price', 100 | 'buy_int_flag']) 101 | 102 | @staticmethod 103 | def generate_random_customer(customer_name: str) -> Customer: 104 | return Customer( 105 | name=customer_name, 106 | attribute_a=random.choice(Customer.ATTRIBUTE_A_MASTER), 107 | attribute_b=random.choice(Customer.ATTRIBUTE_B_MASTER), 108 | attribute_c=random.choice(Customer.ATTRIBUTE_C_MASTER) 109 | ) 110 | 111 | @staticmethod 112 | def generate_random_item(item_name: str) -> Item: 113 | price = random.choice(list(range(10000, 30000, 1000))) 114 | cost = int(price * (1 - 0.1 - random.random() / 5)) 115 | return Item( 116 | name=item_name, 117 | price=price, 118 | cost=cost, 119 | attribute_a=random.choice(Item.ATTRIBUTE_A_MASTER), 120 | attribute_b=random.choice(Item.ATTRIBUTE_B_MASTER), 121 | attribute_c=random.choice(Item.ATTRIBUTE_C_MASTER) 122 | ) 123 | 124 | @staticmethod 125 | def generate_all_coupons() -> List[Coupon]: 126 | return [Coupon(name=f'coupon_{x}', down_price=x) 127 | for x in Coupon.DOWN_PRICE_TYPE_MASTER] 128 | 129 | def _simulate_buy_flg( 130 | self, 131 | customer: Customer, 132 | item: Item, 133 | coupon: Coupon) -> int: 134 | value_price = 0 135 | # value of customer_attribute 136 | value_price += self._customer_attribute_a_scores[customer.attribute_a] 137 | value_price += self._customer_attribute_b_scores[customer.attribute_b] 138 | value_price += self._customer_attribute_c_scores[customer.attribute_c] 139 | # value of item_attribute 140 | value_price += self._item_attribute_a_scores[item.attribute_a] 141 | value_price += self._item_attribute_b_scores[item.attribute_b] 142 | value_price += self._item_attribute_c_scores[item.attribute_c] 143 | # value of matching customer and item 144 | for customer_attribute in [ 145 | customer.attribute_a, 146 | customer.attribute_b, 147 | customer.attribute_c]: 148 | for item_attribute in [ 149 | item.attribute_a, 150 | item.attribute_b, 151 | item.attribute_c]: 152 | value_price += self._attribute_matching_scores[customer_attribute, item_attribute] 153 | # minus price 154 | value_price -= (item.price - coupon.down_price) 155 | # calculate buy_rate 156 | buy_rate = 1 / (1 + np.exp(- (value_price - 3000) / 5000)) 157 | return 1 if random.random() <= buy_rate else 0 158 | 159 | 160 | @dataclass(frozen=True) 161 | class MatchingProblem: 162 | customers: List[Customer] 163 | items: List[Item] 164 | coupons: List[Coupon] 165 | customer_names: List[str] 166 | item_names: List[str] 167 | coupon_names: List[str] 168 | buy_rate_model: tree.DecisionTreeClassifier 169 | max_display_num_per_item: int 170 | max_display_num_per_coupon: int 171 | customer_features_df: pd.DataFrame 172 | item_features_df: pd.DataFrame 173 | coupon_features_df: pd.DataFrame 174 | 175 | 176 | class MatchingProblemGenerator: 177 | 178 | def __init__(self): 179 | pass 180 | 181 | @staticmethod 182 | def generate(customer_num: int, item_num: int) -> MatchingProblem: 183 | # リミット値の計算 184 | max_display_num_per_item = int(customer_num / item_num * 2) 185 | max_display_num_per_coupon = int(customer_num / 4) 186 | 187 | # 学習モデルの生成 188 | data_generator = DataGenerator() 189 | train_df = data_generator.generate_train_dataset(data_num=300000) 190 | model = tree.DecisionTreeClassifier(max_depth=12) 191 | model = model.fit(train_df.drop(columns='buy_int_flag'), train_df['buy_int_flag']) 192 | 193 | customers = [data_generator.generate_random_customer(f'customer_{no}') 194 | for no in range(customer_num)] 195 | items = [data_generator.generate_random_item(f'item_{no}') 196 | for no in range(item_num)] 197 | coupons = data_generator.generate_all_coupons() 198 | 199 | customer_names = [customer.name for customer in customers] 200 | item_names = [item.name for item in items] 201 | coupon_names = [f'coupon_{coupon.down_price}' for coupon in coupons] 202 | 203 | # パラメータをDataFrameに変換 204 | customer_features_df = \ 205 | pd.DataFrame([[customer.attribute_a, customer.attribute_b, customer.attribute_c] 206 | for customer in customers], 207 | index=customer_names, 208 | columns=['customer_attribute_a', 209 | 'customer_attribute_b', 210 | 'customer_attribute_c']) 211 | item_features_df = \ 212 | pd.DataFrame([[item.price, item.cost, 213 | item.attribute_a, item.attribute_b, item.attribute_c] 214 | for item in items], 215 | index=item_names, 216 | columns=['item_price', 217 | 'item_cost', 218 | 'item_attribute_a', 219 | 'item_attribute_b', 220 | 'item_attribute_c']) 221 | coupon_features_df = pd.DataFrame([[coupon.down_price] for coupon in coupons], 222 | index=coupon_names, columns=['coupon_down_price']) 223 | 224 | return MatchingProblem( 225 | customers=customers, 226 | items=items, 227 | coupons=coupons, 228 | customer_names=customer_names, 229 | item_names=item_names, 230 | coupon_names=coupon_names, 231 | buy_rate_model=model, 232 | max_display_num_per_item=max_display_num_per_item, 233 | max_display_num_per_coupon=max_display_num_per_coupon, 234 | customer_features_df=customer_features_df, 235 | item_features_df=item_features_df, 236 | coupon_features_df=coupon_features_df 237 | ) 238 | -------------------------------------------------------------------------------- /sample/usage/pulp/whiskas.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | 17 | from codableopt import Problem, Objective, IntVariable, OptSolver, PenaltyAdjustmentMethod 18 | 19 | 20 | Ingredients = ['CHICKEN', 'BEEF', 'MUTTON', 'RICE', 'WHEAT', 'GEL'] 21 | costs = \ 22 | {'CHICKEN': 0.013, 'BEEF': 0.008, 'MUTTON': 0.010, 'RICE': 0.002, 'WHEAT': 0.005, 'GEL': 0.001} 23 | proteinPercent = \ 24 | {'CHICKEN': 0.100, 'BEEF': 0.200, 'MUTTON': 0.150, 'RICE': 0.000, 'WHEAT': 0.040, 'GEL': 0.000} 25 | fatPercent = \ 26 | {'CHICKEN': 0.080, 'BEEF': 0.100, 'MUTTON': 0.110, 'RICE': 0.010, 'WHEAT': 0.010, 'GEL': 0.000} 27 | fibrePercent = \ 28 | {'CHICKEN': 0.001, 'BEEF': 0.005, 'MUTTON': 0.003, 'RICE': 0.100, 'WHEAT': 0.150, 'GEL': 0.000} 29 | saltPercent = \ 30 | {'CHICKEN': 0.002, 'BEEF': 0.005, 'MUTTON': 0.007, 'RICE': 0.002, 'WHEAT': 0.008, 'GEL': 0.000} 31 | 32 | # 変数を定義 33 | x = [IntVariable(name=f'x_{ingredient}', lower=0, upper=100) for ingredient in Ingredients] 34 | 35 | # 問題を設定 36 | problem = Problem(is_max_problem=False) 37 | 38 | 39 | def objective_function(var_x, para_costs): 40 | return np.dot(var_x, para_costs) 41 | 42 | 43 | # 目的関数を定義 44 | problem += Objective(objective=objective_function, 45 | args_map={'var_x': x, 46 | 'para_costs': [costs[ingredient] for ingredient in Ingredients]}) 47 | 48 | # 制約式を定義 49 | problem += sum(x) == 100 50 | problem += sum([proteinPercent[ingredient] * x_ for x_, ingredient in zip(x, Ingredients)]) >= 8.0 51 | problem += sum([fatPercent[ingredient] * x_ for x_, ingredient in zip(x, Ingredients)]) >= 6.0 52 | problem += sum([fibrePercent[ingredient] * x_ for x_, ingredient in zip(x, Ingredients)]) <= 2.0 53 | problem += sum([saltPercent[ingredient] * x_ for x_, ingredient in zip(x, Ingredients)]) <= 0.4 54 | 55 | 56 | # 問題を確認 57 | print(problem) 58 | 59 | # ソルバーを生成 60 | solver = OptSolver() 61 | 62 | # ソルバー内で使う最適化手法を生成 63 | method = PenaltyAdjustmentMethod(steps=100000) 64 | 65 | # 最適化実施 66 | answer, is_feasible = solver.solve(problem, method) 67 | print(f'answer:{answer}, answer_is_feasible:{is_feasible}') 68 | -------------------------------------------------------------------------------- /sample/usage/pulp/whiskas_pulp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from pulp import * 16 | 17 | Ingredients = ['CHICKEN', 'BEEF', 'MUTTON', 'RICE', 'WHEAT', 'GEL'] 18 | 19 | costs = \ 20 | {'CHICKEN': 0.013, 'BEEF': 0.008, 'MUTTON': 0.010, 'RICE': 0.002, 'WHEAT': 0.005, 'GEL': 0.001} 21 | proteinPercent = \ 22 | {'CHICKEN': 0.100, 'BEEF': 0.200, 'MUTTON': 0.150, 'RICE': 0.000, 'WHEAT': 0.040, 'GEL': 0.000} 23 | fatPercent = \ 24 | {'CHICKEN': 0.080, 'BEEF': 0.100, 'MUTTON': 0.110, 'RICE': 0.010, 'WHEAT': 0.010, 'GEL': 0.000} 25 | fibrePercent = \ 26 | {'CHICKEN': 0.001, 'BEEF': 0.005, 'MUTTON': 0.003, 'RICE': 0.100, 'WHEAT': 0.150, 'GEL': 0.000} 27 | saltPercent = \ 28 | {'CHICKEN': 0.002, 'BEEF': 0.005, 'MUTTON': 0.007, 'RICE': 0.002, 'WHEAT': 0.008, 'GEL': 0.000} 29 | 30 | prob = LpProblem('The Whiskas Problem', LpMinimize) 31 | ingredient_vars = LpVariable.dicts('Ingr', Ingredients, 0) 32 | 33 | prob += lpSum([costs[i] * ingredient_vars[i] for i in Ingredients]) 34 | 35 | prob += lpSum([ingredient_vars[i] for i in Ingredients]) == 100 36 | prob += lpSum([proteinPercent[i] * ingredient_vars[i] for i in Ingredients]) >= 8.0 37 | prob += lpSum([fatPercent[i] * ingredient_vars[i] for i in Ingredients]) >= 6.0 38 | prob += lpSum([fibrePercent[i] * ingredient_vars[i] for i in Ingredients]) <= 2.0 39 | prob += lpSum([saltPercent[i] * ingredient_vars[i] for i in Ingredients]) <= 0.4 40 | 41 | prob.solve() 42 | 43 | for v in prob.variables(): 44 | print(f'{v.name}={v.varValue}') 45 | print(f'obj={value(prob.objective)}') 46 | -------------------------------------------------------------------------------- /sample/usage/sample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/recruit-tech/codable-model-optimizer/4aa211c5c03558941812f7e10b8351f4ae45e212/sample/usage/sample/__init__.py -------------------------------------------------------------------------------- /sample/usage/sample/basic.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | 17 | from codableopt import Problem, Objective, IntVariable, DoubleVariable, \ 18 | CategoryVariable, OptSolver, PenaltyAdjustmentMethod 19 | 20 | # 変数を定義 21 | x = IntVariable(name='x', lower=np.double(0.0), upper=np.double(2)) 22 | y = DoubleVariable(name='y', lower=np.double(0.0), upper=None) 23 | z = CategoryVariable(name='z', categories=['a', 'b', 'c']) 24 | 25 | 26 | # 目的関数に指定する関数 27 | def objective_function(var_x, var_y, var_z, parameters): 28 | obj_value = parameters['coef_x'] * var_x + parameters['coef_y'] * var_y 29 | 30 | if var_z == 'a': 31 | obj_value += 10.0 32 | elif var_z == 'b': 33 | obj_value += 8.0 34 | else: 35 | # var_z == 'c' 36 | obj_value -= 3.0 37 | 38 | return obj_value 39 | 40 | 41 | # 問題を設定 42 | problem = Problem(is_max_problem=True) 43 | 44 | # 目的関数を定義 45 | problem += Objective(objective=objective_function, 46 | args_map={'var_x': x, 'var_y': y, 'var_z': z, 47 | 'parameters': {'coef_x': -3.0, 'coef_y': 4.0}}) 48 | 49 | # 制約式を定義 50 | problem += 2 * x + 4 * y + 2 * (z == 'a') + 3 * (z == ('b', 'c')) <= 8 51 | problem += 2 * x - y + 2 * (z == 'b') > 3 52 | 53 | # 問題を確認 54 | print(problem) 55 | 56 | # ソルバーを生成 57 | solver = OptSolver() 58 | 59 | # ソルバー内で使う最適化手法を生成 60 | method = PenaltyAdjustmentMethod(steps=40000) 61 | 62 | # 最適化実施 63 | answer, is_feasible = solver.solve(problem, method) 64 | print(f'answer:{answer}, answer_is_feasible:{is_feasible}') 65 | -------------------------------------------------------------------------------- /sample/usage/sample/how_to_use_delta_objective.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | 17 | from codableopt import Problem, Objective, IntVariable, DoubleVariable, \ 18 | CategoryVariable, OptSolver, PenaltyAdjustmentMethod 19 | 20 | # 変数を定義 21 | x = IntVariable(name='x', lower=0, upper=None) 22 | y = DoubleVariable(name='y', lower=np.double(0.0), upper=None) 23 | z = CategoryVariable(name='z', categories=['a', 'b', 'c']) 24 | 25 | 26 | # 目的関数に指定する関数 27 | def objective_function(var_x, var_y, var_z, parameters): 28 | value = parameters['coef_x'] * var_x + parameters['coef_y'] * var_y 29 | 30 | if var_z == 'a': 31 | value += 10.0 32 | elif var_z == 'b': 33 | value += 8.0 34 | else: 35 | # var_z == 'c' 36 | value -= 3.0 37 | 38 | return value 39 | 40 | 41 | # 差分計算による目的関数に指定する関数 42 | def delta_objective_function( 43 | pre_var_x, 44 | pre_var_y, 45 | pre_var_z, 46 | var_x, 47 | var_y, 48 | var_z, 49 | parameters): 50 | delta_value = 0 51 | if pre_var_x != var_x: 52 | delta_value += parameters['coef_x'] * (var_x - pre_var_x) 53 | 54 | if pre_var_y != var_y: 55 | delta_value += parameters['coef_y'] * (var_y - pre_var_y) 56 | 57 | if pre_var_z != var_z: 58 | if pre_var_z == 'a': 59 | delta_value -= 10.0 60 | elif pre_var_z == 'b': 61 | delta_value -= 8.0 62 | else: 63 | # pre_z == 'c' 64 | delta_value += 3.0 65 | 66 | if var_z == 'a': 67 | delta_value += 10.0 68 | elif var_z == 'b': 69 | delta_value += 8.0 70 | else: 71 | # z == 'c' 72 | delta_value -= 3.0 73 | 74 | return delta_value 75 | 76 | 77 | # 問題を設定 78 | problem = Problem(is_max_problem=True) 79 | 80 | # 目的関数を定義 81 | problem += Objective(objective=objective_function, 82 | args_map={'var_x': x, 'var_y': y, 'var_z': z, 83 | 'parameters': {'coef_x': -3.0, 'coef_y': 4.0}}, 84 | delta_objective=delta_objective_function) 85 | 86 | # 制約式を定義 87 | problem += 2 * x + 4 * y + 2 * (z == 'a') + 3 * (z == ('b', 'c')) <= 8 88 | problem += 2 * x - y + 2 * (z == 'b') > 3 89 | 90 | # ソルバーを生成 91 | solver = OptSolver(round_times=2, debug=True, debug_unit_step=1000) 92 | 93 | # ソルバー内で使う最適化手法を生成 94 | method = PenaltyAdjustmentMethod(steps=40000) 95 | 96 | # 最適化実施 97 | answer, is_feasible = solver.solve(problem, method) 98 | print(f'answer:{answer}, answer_is_feasible:{is_feasible}') 99 | -------------------------------------------------------------------------------- /sample/usage/sample/how_to_use_init_answers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | 17 | from codableopt import Problem, Objective, IntVariable, DoubleVariable, \ 18 | CategoryVariable, OptSolver, PenaltyAdjustmentMethod 19 | 20 | # 変数を定義 21 | x = IntVariable(name='x', lower=np.double(0.0), upper=np.double(2)) 22 | y = DoubleVariable(name='y', lower=np.double(0.0), upper=None) 23 | z = CategoryVariable(name='z', categories=['a', 'b', 'c']) 24 | 25 | 26 | # 目的関数に指定する関数 27 | def objective_function(var_x, var_y, var_z, parameters): 28 | obj_value = parameters['coef_x'] * var_x + parameters['coef_y'] * var_y 29 | 30 | if var_z == 'a': 31 | obj_value += 10.0 32 | elif var_z == 'b': 33 | obj_value += 8.0 34 | else: 35 | # var_z == 'c' 36 | obj_value -= 3.0 37 | 38 | return obj_value 39 | 40 | 41 | # 問題を設定 42 | problem = Problem(is_max_problem=True) 43 | 44 | # 目的関数を定義 45 | problem += Objective(objective=objective_function, 46 | args_map={'var_x': x, 'var_y': y, 'var_z': z, 47 | 'parameters': {'coef_x': -3.0, 'coef_y': 4.0}}) 48 | 49 | # 制約式を定義 50 | problem += 2 * x + 4 * y + 2 * (z == 'a') + 3 * (z == ('b', 'c')) <= 8 51 | problem += 2 * x - y + 2 * (z == 'b') > 3 52 | 53 | # 問題を確認 54 | print(problem) 55 | 56 | # ソルバーを生成 57 | solver = OptSolver() 58 | 59 | # ソルバー内で使う最適化手法を生成 60 | method = PenaltyAdjustmentMethod(steps=40000) 61 | 62 | # 初期解を指定 63 | init_answers = [ 64 | {'x': 0, 'y': 1, 'z': 'a'} 65 | ] 66 | 67 | # 最適化実施 68 | answer, is_feasible = solver.solve(problem, method, init_answers=init_answers) 69 | print(f'answer:{answer}, answer_is_feasible:{is_feasible}') 70 | -------------------------------------------------------------------------------- /sample/usage/sample/how_to_use_user_define_constraint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | 17 | from codableopt import Problem, Objective, IntVariable, DoubleVariable, \ 18 | CategoryVariable, OptSolver, PenaltyAdjustmentMethod, UserDefineConstraint 19 | 20 | # 変数を定義 21 | x = IntVariable(name='x', lower=np.double(0.0), upper=np.double(2)) 22 | y = DoubleVariable(name='y', lower=np.double(0.0), upper=None) 23 | z = CategoryVariable(name='z', categories=['a', 'b', 'c']) 24 | 25 | 26 | # 目的関数に指定する関数 27 | def objective_function(var_x, var_y, var_z, parameters): 28 | obj_value = parameters['coef_x'] * var_x + parameters['coef_y'] * var_y 29 | 30 | if var_z == 'a': 31 | obj_value += 10.0 32 | elif var_z == 'b': 33 | obj_value += 8.0 34 | else: 35 | # var_z == 'c' 36 | obj_value -= 3.0 37 | 38 | return obj_value 39 | 40 | 41 | # 問題を設定 42 | problem = Problem(is_max_problem=True) 43 | 44 | # 目的関数を定義 45 | problem += Objective(objective=objective_function, 46 | args_map={'var_x': x, 'var_y': y, 'var_z': z, 47 | 'parameters': {'coef_x': -3.0, 'coef_y': 4.0}}) 48 | 49 | 50 | # 制約式を定義 51 | def udf_constraint_function(var_x, var_y, var_z): 52 | violation_amount = 2 * var_x + 4 * var_y - 8 53 | if var_z == 'a': 54 | violation_amount += 2 55 | else: 56 | violation_amount += 3 57 | 58 | if violation_amount <= 0: 59 | return 0 60 | else: 61 | return violation_amount 62 | 63 | 64 | # 現状、UserDefineConstraintは非推奨の機能 65 | constant = UserDefineConstraint(udf_constraint_function, 66 | args_map={'var_x': x, 'var_y': y, 'var_z': z}, 67 | constraint_name='user_define_constraint') 68 | problem += constant 69 | problem += 2 * x - y + 2 * (z == 'b') > 3 70 | 71 | # 問題を確認 72 | print(problem) 73 | 74 | # ソルバーを生成 75 | solver = OptSolver(debug=True, debug_unit_step=1000) 76 | 77 | # ソルバー内で使う最適化手法を生成 78 | method = PenaltyAdjustmentMethod(steps=40000, proposed_rate_of_random_movement=1) 79 | 80 | # 最適化実施 81 | answer, is_feasible = solver.solve(problem, method) 82 | print(f'answer:{answer}, answer_is_feasible:{is_feasible}') 83 | -------------------------------------------------------------------------------- /sample/usage/sample/knapsack.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import random 16 | 17 | from codableopt import Problem, Objective, IntVariable, OptSolver, \ 18 | PenaltyAdjustmentMethod 19 | 20 | 21 | # アイテム数、最大重量を設定 22 | item_num = 40 23 | max_weight = 1000 24 | # アイテム名を生成 25 | item_names = [f'item_{no}' for no in range(item_num)] 26 | # アイテムのバリューと重量を設定 27 | parameter_item_values = [random.randint(10, 50) for _ in item_names] 28 | parameter_item_weights = [random.randint(20, 40) for _ in item_names] 29 | 30 | # アイテムのBool変数を定義 31 | var_item_flags = [IntVariable(name=item_name, lower=0, upper=1) for item_name in item_names] 32 | 33 | 34 | # 目的関数として、距離を計算する関数を定義 35 | def calculate_total_values(item_flags, item_values): 36 | return sum([flag * value for flag, value in zip(item_flags, item_values)]) 37 | 38 | 39 | # 問題を設定 40 | problem = Problem(is_max_problem=True) 41 | 42 | # 目的関数を定義 43 | problem += Objective(objective=calculate_total_values, 44 | args_map={'item_flags': var_item_flags, 'item_values': parameter_item_values}) 45 | 46 | # 重量制限の制約式を追加 47 | problem += sum([item_flag * weight for item_flag, 48 | weight in zip(var_item_flags, 49 | parameter_item_weights)]) <= max_weight 50 | 51 | # 最適化実施 52 | solver = OptSolver(round_times=4, debug=True, debug_unit_step=1000) 53 | method = PenaltyAdjustmentMethod(steps=10000) 54 | answer, is_feasible = solver.solve(problem, method, n_jobs=-1) 55 | 56 | print(f'answer_is_feasible:{is_feasible}') 57 | print(f'select_items: 'f'{", ".join([x for x in answer.keys() if answer[x] == 1])}') 58 | -------------------------------------------------------------------------------- /sample/usage/sample/marketing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import numpy as np 16 | 17 | from codableopt import Problem, Objective, IntVariable, OptSolver, \ 18 | PenaltyAdjustmentMethod 19 | 20 | 21 | # 顧客数、CM数を設定 22 | CUSTOMER_NUM = 1000 23 | CM_NUM = 100 24 | SELECTED_CM_LIMIT = 10 25 | # 顧客がCMを見る確率を生成 26 | view_rates = np.random.rand(CUSTOMER_NUM, CM_NUM) / 10 / SELECTED_CM_LIMIT 27 | 28 | # CMの放送有無の変数を定義 29 | cm_times = [IntVariable(name=f'cm_{no}', lower=0, upper=1) for no in range(CM_NUM)] 30 | 31 | # 問題を設定 32 | problem = Problem(is_max_problem=True) 33 | 34 | 35 | # 目的関数として、CMを1度でも見る確率を計算 36 | def calculate_view_rate_sum(var_cm_times, para_non_view_rates): 37 | selected_cm_noes = \ 38 | [cm_no for cm_no, var_cm_time in enumerate(var_cm_times) if var_cm_time == 1] 39 | view_rate_per_customers = np.ones(para_non_view_rates.shape[0]) \ 40 | - np.prod(para_non_view_rates[:, selected_cm_noes], axis=1) 41 | return np.sum(view_rate_per_customers) 42 | 43 | 44 | # 目的関数を定義 45 | problem += Objective(objective=calculate_view_rate_sum, 46 | args_map={'var_cm_times': cm_times, 47 | 'para_non_view_rates': np.ones(view_rates.shape) - view_rates}) 48 | 49 | # CMの選択数の制約式を追加 50 | problem += sum(cm_times) <= SELECTED_CM_LIMIT 51 | 52 | print(problem) 53 | 54 | # 最適化実施 55 | solver = OptSolver(round_times=2, debug=True, debug_unit_step=1000) 56 | method = PenaltyAdjustmentMethod(steps=100000) 57 | answer, is_feasible = solver.solve(problem, method, n_jobs=-1) 58 | 59 | print(f'answer_is_feasible:{is_feasible}') 60 | print(f'selected cm: {[cm_name for cm_name in answer.keys() if answer[cm_name] == 1]}') 61 | -------------------------------------------------------------------------------- /sample/usage/sample/matching.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List 16 | 17 | import pandas as pd 18 | 19 | from sample.usage.problem.matching_problem_generator import MatchingProblemGenerator 20 | from codableopt import Problem, Objective, CategoryVariable, OptSolver, PenaltyAdjustmentMethod 21 | 22 | 23 | # 最適化問題定義 24 | print('generate Problem') 25 | matching = MatchingProblemGenerator.generate(customer_num=1000, item_num=20) 26 | 27 | print('start Optimization') 28 | # 変数を定義 29 | selected_items = \ 30 | [CategoryVariable(name=f'item_for_{customer.name}', categories=matching.item_names) 31 | for customer in matching.customers] 32 | selected_coupons = \ 33 | [CategoryVariable(name=f'coupon_for_{customer.name}', categories=matching.coupon_names) 34 | for customer in matching.customers] 35 | 36 | 37 | # 利益の期待値を計算する関数、目的関数に利用 38 | def calculate_benefit( 39 | var_selected_items: List[str], 40 | var_selected_coupons: List[str], 41 | para_customer_features_df: pd.DataFrame, 42 | para_item_features_df: pd.DataFrame, 43 | para_coupon_features_df: pd.DataFrame, 44 | para_buy_rate_model): 45 | 46 | features_df = pd.concat([ 47 | para_customer_features_df.reset_index(drop=True), 48 | para_item_features_df.loc[var_selected_items, :].reset_index(drop=True), 49 | para_coupon_features_df.loc[var_selected_coupons, :].reset_index(drop=True) 50 | ], axis=1) 51 | 52 | # 目的関数内で機械学習モデルを利用 53 | buy_rate = \ 54 | [x[1] for x in para_buy_rate_model.predict_proba(features_df.drop(columns='item_cost'))] 55 | 56 | return sum( 57 | buy_rate * 58 | (features_df['item_price'] - features_df['item_cost'] - features_df['coupon_down_price'])) 59 | 60 | 61 | # 問題を設定 62 | problem = Problem(is_max_problem=True) 63 | 64 | # 目的関数を定義 65 | problem += Objective(objective=calculate_benefit, 66 | args_map={ 67 | 'var_selected_items': selected_items, 68 | 'var_selected_coupons': selected_coupons, 69 | 'para_customer_features_df': matching.customer_features_df, 70 | 'para_item_features_df': matching.item_features_df, 71 | 'para_coupon_features_df': matching.coupon_features_df, 72 | 'para_buy_rate_model': matching.buy_rate_model 73 | }) 74 | 75 | # 制約式を定義 76 | for item in matching.items: 77 | # 必ず1人以上のカスタマーにアイテムを表示する制約式を設定 78 | problem += sum([(x == item.name) for x in selected_items]) >= 1 79 | # 同じアイテムの最大表示人数を制限する制約式を設定 80 | problem += sum([(x == item.name) for x in selected_items]) <= matching.max_display_num_per_item 81 | 82 | for coupon in matching.coupons: 83 | # クーポンの最大発行数の制約式を設定 84 | problem += \ 85 | sum([(x == coupon.name) for x in selected_coupons]) <= matching.max_display_num_per_coupon 86 | 87 | 88 | # 最適化実施 89 | print('start solve') 90 | solver = OptSolver(round_times=1, debug=True, debug_unit_step=1000, 91 | num_to_tune_penalty=100, num_to_select_init_answer=1) 92 | method = PenaltyAdjustmentMethod(steps=40000) 93 | answer, is_feasible = solver.solve(problem, method) 94 | 95 | print(f'answer_is_feasible:{is_feasible}') 96 | print(answer) 97 | -------------------------------------------------------------------------------- /sample/usage/sample/sharing_clustering.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import random 16 | 17 | from codableopt import Problem, Objective, CategoryVariable, OptSolver, PenaltyAdjustmentMethod 18 | 19 | 20 | # 問題のパラメータ設定 21 | CUSTOMER_NUM = 30 22 | LIMIT_NUM_PER_taxi = 4 23 | TAXI_NUM = 10 24 | 25 | customer_names = [f'CUS_{i}' for i in range(CUSTOMER_NUM)] 26 | taxi_names = [f'taxi_{i}' for i in range(TAXI_NUM)] 27 | 28 | # 年齢・性別のマッチング 29 | customers_age = [random.choice(['20-30', '30-60', '60-']) for _ in customer_names] 30 | customers_sex = [random.choice(['m', 'f']) for _ in customer_names] 31 | 32 | # 顧客の車割り当て変数を作成 33 | x = [CategoryVariable(name=x, categories=taxi_names) for x in customer_names] 34 | 35 | # 問題を設定 36 | problem = Problem(is_max_problem=True) 37 | 38 | 39 | # 目的関数として、距離を計算する関数を定義 40 | def calc_matching_score(var_x, para_taxi_names, para_customers_age, para_customers_sex): 41 | score = 0 42 | for para_taxi_name in para_taxi_names: 43 | customers_in_taxi = [(age, sex) for var_bit_x, age, sex 44 | in zip(var_x, para_customers_age, para_customers_sex) 45 | if var_bit_x == para_taxi_name] 46 | num_in_taxi = len(customers_in_taxi) 47 | if num_in_taxi > 1: 48 | score += num_in_taxi - len(set([age for age, _ in customers_in_taxi])) 49 | score += num_in_taxi - len(set([sex for _, sex in customers_in_taxi])) 50 | 51 | return score 52 | 53 | 54 | # 目的関数を定義 55 | problem += Objective(objective=calc_matching_score, 56 | args_map={'var_x': x, 57 | 'para_taxi_names': taxi_names, 58 | 'para_customers_age': customers_age, 59 | 'para_customers_sex': customers_sex}) 60 | 61 | # 必ず1度以上、全てのポイントに到達する制約式を追加 62 | for taxi_name in taxi_names: 63 | problem += sum([(bit_x == taxi_name) for bit_x in x]) <= LIMIT_NUM_PER_taxi 64 | 65 | # 最適化実施 66 | solver = OptSolver(round_times=4, debug=True, debug_unit_step=1000) 67 | method = PenaltyAdjustmentMethod(steps=10000) 68 | answer, is_feasible = solver.solve(problem, method, n_jobs=-1) 69 | 70 | print(f'answer_is_feasible:{is_feasible}') 71 | print(f'answer: {answer}') 72 | -------------------------------------------------------------------------------- /sample/usage/sample/tsp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import random 16 | import math 17 | from itertools import combinations 18 | 19 | from codableopt import Problem, Objective, CategoryVariable, OptSolver, PenaltyAdjustmentMethod 20 | 21 | 22 | # 距離生成関数 23 | def generate_distances(args_place_names): 24 | # ポイント間の距離を生成 25 | tmp_coordinates = {} 26 | for x in ['start'] + args_place_names: 27 | tmp_coordinates[x] = (random.randint(1, 1000), random.randint(1, 1000)) 28 | 29 | generated_distances = {} 30 | for point_to_point in combinations(['start'] + args_place_names, 2): 31 | coordinate_a = tmp_coordinates[point_to_point[0]] 32 | coordinate_b = tmp_coordinates[point_to_point[1]] 33 | distance_value = math.sqrt(math.pow(coordinate_a[0] - coordinate_b[0], 2) + 34 | math.pow(coordinate_a[1] - coordinate_b[1], 2)) 35 | generated_distances[point_to_point] = distance_value 36 | generated_distances[tuple(reversed(point_to_point))] = distance_value 37 | for x in ['start'] + args_place_names: 38 | generated_distances[(x, x)] = 0 39 | 40 | return generated_distances 41 | 42 | 43 | # 単純なTSP問題 44 | # ルート/ポイント数を設定 45 | PLACE_NUM = 30 46 | # 行き先名を生成 47 | destination_names = [f'destination_{no}' for no in range(PLACE_NUM)] 48 | # ポイント名を生成 49 | place_names = [f'P{no}' for no in range(PLACE_NUM)] 50 | # 距離を生成 51 | distances = generate_distances(place_names) 52 | 53 | # ルート変数を定義 54 | destinations = [CategoryVariable(name=destination_name, categories=place_names) 55 | for destination_name in destination_names] 56 | 57 | # 問題を設定 58 | problem = Problem(is_max_problem=False) 59 | 60 | 61 | # 目的関数として、距離を計算する関数を定義 62 | def calc_distance(var_destinations, para_distances): 63 | return sum([para_distances[(x, y)] for x, y 64 | in zip(['start'] + var_destinations, var_destinations + ['start'])]) 65 | 66 | 67 | # 目的関数を定義 68 | problem += Objective(objective=calc_distance, 69 | args_map={'var_destinations': destinations, 'para_distances': distances}) 70 | 71 | # 必ず1度以上、全てのポイントに到達する制約式を追加 72 | for place_name in place_names: 73 | problem += sum([(destination == place_name) 74 | for destination in destinations]) >= 1 75 | 76 | # 最適化実施 77 | solver = OptSolver(round_times=4, debug=True, debug_unit_step=1000) 78 | method = PenaltyAdjustmentMethod(steps=50000) 79 | answer, is_feasible = solver.solve(problem, method, n_jobs=-1) 80 | 81 | print(f'answer_is_feasible:{is_feasible}') 82 | root = ['start'] + [answer[root] for root in destination_names] + ['start'] 83 | print(f'root: {" -> ".join(root)}') 84 | -------------------------------------------------------------------------------- /sample/usage/sample/tsp2.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Recruit Co., Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import random 16 | import math 17 | from itertools import combinations 18 | 19 | from codableopt import Problem, Objective, CategoryVariable, OptSolver, \ 20 | PenaltyAdjustmentMethod 21 | 22 | 23 | # 時間帯(出発したからのトータル距離の値範囲)によって距離が変化するTSP問題 24 | # ルート/ポイント数を設定 25 | PLACE_NUM = 30 26 | # ルート名を生成 27 | destination_names = [f'destination_{no}' for no in range(PLACE_NUM)] 28 | # ポイント名を生成 29 | place_names = [f'P{no}' for no in range(PLACE_NUM)] 30 | 31 | 32 | # 距離生成関数 33 | def generate_distances(args_place_names): 34 | # ポイント間の距離を生成 35 | tmp_coordinates = {} 36 | for x in ['start'] + args_place_names: 37 | tmp_coordinates[x] = (random.randint(1, 1000), random.randint(1, 1000)) 38 | 39 | generated_distances = {} 40 | for point_to_point in combinations(['start'] + args_place_names, 2): 41 | coordinate_a = tmp_coordinates[point_to_point[0]] 42 | coordinate_b = tmp_coordinates[point_to_point[1]] 43 | distance_value = math.sqrt(math.pow(coordinate_a[0] - coordinate_b[0], 2) + 44 | math.pow(coordinate_a[1] - coordinate_b[1], 2)) 45 | generated_distances[point_to_point] = distance_value 46 | generated_distances[tuple(reversed(point_to_point))] = distance_value 47 | for x in ['start'] + args_place_names: 48 | generated_distances[(x, x)] = 0 49 | 50 | return generated_distances 51 | 52 | 53 | # 朝の時間帯(Startからの出発地点までの合計距離が、0以上300以下の間)におけるポイント間の距離を生成 54 | morning_distances = generate_distances(place_names) 55 | # 昼の時間帯(Startからの出発地点までの合計距離が、301以上700以下の間)におけるポイント間の距離を生成 56 | noon_distances = generate_distances(place_names) 57 | # 夜の時間帯(Startからの出発地点までの合計距離が、701以上)におけるポイント間の距離を生成 58 | night_distances = generate_distances(place_names) 59 | 60 | # ルート変数を定義 61 | destinations = [CategoryVariable(name=destination_name, categories=place_names) 62 | for destination_name in destination_names] 63 | 64 | # 問題を設定 65 | problem = Problem(is_max_problem=False) 66 | 67 | 68 | # 目的関数として、距離を計算する関数を定義 69 | def calc_distance( 70 | var_destinations, 71 | para_morning_distances, 72 | para_noon_distances, 73 | para_night_distances): 74 | distance = 0 75 | 76 | for place_from, place_to in zip( 77 | ['start'] + var_destinations, var_destinations + ['start']): 78 | # 朝の時間帯(Startからの出発地点までの合計距離が、0以上300以下の間) 79 | if distance <= 300: 80 | distance += para_morning_distances[(place_from, place_to)] 81 | # 昼の時間帯(Startからの出発地点までの合計距離が、301以上700以下の間) 82 | elif distance <= 700: 83 | distance += para_noon_distances[(place_from, place_to)] 84 | # 夜の時間帯(Startからの出発地点までの合計距離が、701以上) 85 | else: 86 | distance += para_night_distances[(place_from, place_to)] 87 | 88 | return distance 89 | 90 | 91 | # 目的関数を定義 92 | problem += Objective(objective=calc_distance, 93 | args_map={'var_destinations': destinations, 94 | 'para_morning_distances': morning_distances, 95 | 'para_noon_distances': noon_distances, 96 | 'para_night_distances': night_distances}) 97 | 98 | # 必ず1度以上、全てのポイントに到達する制約式を追加 99 | for place_name in place_names: 100 | problem += sum([(destination == place_name) 101 | for destination in destinations]) >= 1 102 | 103 | # 最適化実施 104 | solver = OptSolver(round_times=4, debug=True, debug_unit_step=1000) 105 | method = PenaltyAdjustmentMethod(steps=50000) 106 | answer, is_feasible = solver.solve(problem, method, n_jobs=-1) 107 | 108 | print(f'answer_is_feasible:{is_feasible}') 109 | root = ["start"] + [answer[x] for x in destination_names] + ["start"] 110 | print(f'root: {" -> ".join(root)}') 111 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | import os 4 | 5 | 6 | class PackageInfo(object): 7 | def __init__(self, info_file): 8 | with open(info_file) as f: 9 | exec(f.read(), self.__dict__) 10 | self.__dict__.pop('__builtins__', None) 11 | 12 | def __getattribute__(self, name): 13 | return super(PackageInfo, self).__getattribute__(name) 14 | 15 | 16 | package_info = PackageInfo(os.path.join('codableopt', 'package_info.py')) 17 | 18 | setup( 19 | name=package_info.__package_name__, 20 | version=package_info.__version__, 21 | description=package_info.__description__, 22 | long_description=open('README.rst').read(), 23 | author=package_info.__author_names__, 24 | author_email=package_info.__author_emails__, 25 | maintainer=package_info.__maintainer_names__, 26 | maintainer_email=package_info.__maintainer_emails__, 27 | url=package_info.__repository_url__, 28 | download_url=package_info.__download_url__, 29 | license=package_info.__license__, 30 | packages=find_packages(exclude='sample'), 31 | keywords=package_info.__keywords__, 32 | zip_safe=False, 33 | install_requires=['numpy>=1.21.0', 'loky>=3.3.0'], 34 | python_requires='>=3.7, <3.11' 35 | ) 36 | --------------------------------------------------------------------------------