├── requirements.txt ├── docs ├── _static │ ├── cbx-logo.ico │ ├── cbx-logo.png │ ├── cbx_py32x32.ico │ └── css │ │ └── style.css ├── examples │ ├── JOSS │ │ ├── JOSS.pdf │ │ ├── initial.txt │ │ └── JOSS_Example.py │ ├── time_benchmarks │ │ ├── profiling.py │ │ ├── objective.py │ │ └── rng.py │ ├── nns │ │ └── models.py │ ├── sampling.ipynb │ ├── custom_noise.ipynb │ └── success_evaluation.ipynb ├── _templates │ └── classtemplate.rst ├── api │ ├── generated │ │ ├── cbx.dynamics.CBO.rst │ │ ├── cbx.dynamics.CBS.rst │ │ ├── cbx.dynamics.PSO.rst │ │ ├── cbx.dynamics.PolarCBO.rst │ │ ├── cbx.objectives.Ackley.rst │ │ ├── cbx.dynamics.CBOMemory.rst │ │ ├── cbx.scheduler.multiply.rst │ │ ├── cbx.dynamics.CBXDynamic.rst │ │ ├── cbx.scheduler.scheduler.rst │ │ ├── cbx.utils.history.track.rst │ │ ├── cbx.objectives.Himmelblau.rst │ │ ├── cbx.objectives.McCormick.rst │ │ ├── cbx.objectives.Rastrigin.rst │ │ ├── cbx.objectives.Rosenbrock.rst │ │ ├── cbx.objectives.snowflake.rst │ │ ├── cbx.plotting.plot_dynamic.rst │ │ ├── cbx.utils.history.track_x.rst │ │ ├── cbx.scheduler.param_update.rst │ │ ├── cbx.dynamics.ParticleDynamic.rst │ │ ├── cbx.utils.history.track_drift.rst │ │ ├── cbx.utils.history.track_energy.rst │ │ ├── cbx.utils.resampling.resampling.rst │ │ ├── cbx.objectives.Ackley_multimodal.rst │ │ ├── cbx.objectives.three_hump_camel.rst │ │ ├── cbx.utils.history.track_consensus.rst │ │ ├── cbx.plotting.plot_dynamic_history.rst │ │ ├── cbx.utils.history.track_drift_mean.rst │ │ ├── cbx.utils.termination.max_it_term.rst │ │ ├── cbx.objectives.Rastrigin_multimodal.rst │ │ ├── cbx.scheduler.effective_sample_size.rst │ │ ├── cbx.utils.history.track_update_norm.rst │ │ ├── cbx.utils.termination.diff_tol_term.rst │ │ ├── cbx.utils.termination.max_eval_term.rst │ │ ├── cbx.utils.termination.max_time_term.rst │ │ ├── cbx.utils.termination.energy_tol_term.rst │ │ ├── cbx.utils.particle_init.init_particles.rst │ │ ├── cbx.utils.objective_handling.cbx_objective.rst │ │ ├── cbx.utils.resampling.loss_update_resampling.rst │ │ ├── cbx.utils.objective_handling.cbx_objective_f1D.rst │ │ ├── cbx.utils.objective_handling.cbx_objective_f2D.rst │ │ ├── cbx.utils.objective_handling.cbx_objective_fh.rst │ │ └── cbx.utils.resampling.ensemble_update_resampling.rst │ ├── index.rst │ ├── plotting.rst │ ├── scheduler.rst │ ├── objectives.rst │ ├── dynamic.rst │ └── utils.rst ├── userguide │ ├── generated │ │ ├── cbx.objectives.Ackley.rst │ │ ├── cbx.objectives.Bukin6.rst │ │ ├── cbx.objectives.Easom.rst │ │ ├── cbx.objectives.McCormick.rst │ │ ├── cbx.objectives.Rastrigin.rst │ │ ├── cbx.objectives.drop_wave.rst │ │ ├── cbx.objectives.eggholder.rst │ │ ├── cbx.objectives.snowflake.rst │ │ ├── cbx.objectives.Himmelblau.rst │ │ ├── cbx.objectives.Michalewicz.rst │ │ ├── cbx.objectives.Rosenbrock.rst │ │ ├── cbx.objectives.Holder_table.rst │ │ ├── cbx.objectives.cross_in_tray.rst │ │ ├── cbx.objectives.three_hump_camel.rst │ │ ├── cbx.objectives.Ackley_multimodal.rst │ │ └── cbx.objectives.Rastrigin_multimodal.rst │ ├── examples.rst │ ├── objectives.rst │ ├── index.rst │ ├── sched.rst │ └── plotting.rst ├── make.bat ├── index.rst └── conf.py ├── cbx ├── utils │ ├── __init__.py │ ├── particle_init.py │ ├── success.py │ ├── objective_handling.py │ ├── termination.py │ ├── history.py │ ├── resampling.py │ └── torch_utils.py ├── __init__.py ├── dynamics │ ├── __init__.py │ ├── regcombinationcbo.py │ ├── cbs.py │ ├── cbo.py │ ├── driftconstrainedcbo.py │ ├── adamcbo.py │ ├── cbo_memory.py │ ├── hypersurfacecbo.py │ └── pso.py ├── correction.py ├── regularizers.py ├── constraints.py ├── scheduler.py └── noise.py ├── tests ├── dynamics │ ├── test_pso.py │ ├── test_adam_cbo.py │ ├── test_cbo_memory.py │ ├── test_hypersurface_cbo.py │ ├── test_driftconstrained_cbo.py │ ├── test_regcombination_cbo.py │ ├── test_cbs.py │ ├── test_mirror_cbo.py │ ├── test_abstraction.py │ ├── test_polarcbo.py │ ├── test_pdyn.py │ └── test_cbo.py ├── scheduler │ ├── test_effective_sample_size.py │ └── test_scheduler.py ├── utils │ ├── test_objective_handeling.py │ └── test_particle_init.py ├── plotting │ └── test_plotting.py └── objectives │ └── test_objectives.py ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── Sphinx.yml │ ├── Tests.yml │ └── codecov.yml ├── LICENSE ├── pyproject.toml ├── citation.cff ├── .gitignore ├── CONTRIBUTING.md └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib 4 | -------------------------------------------------------------------------------- /docs/_static/cbx-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PdIPS/CBXpy/HEAD/docs/_static/cbx-logo.ico -------------------------------------------------------------------------------- /docs/_static/cbx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PdIPS/CBXpy/HEAD/docs/_static/cbx-logo.png -------------------------------------------------------------------------------- /docs/_static/cbx_py32x32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PdIPS/CBXpy/HEAD/docs/_static/cbx_py32x32.ico -------------------------------------------------------------------------------- /docs/examples/JOSS/JOSS.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PdIPS/CBXpy/HEAD/docs/examples/JOSS/JOSS.pdf -------------------------------------------------------------------------------- /cbx/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .particle_init import init_particles 2 | from . import resampling 3 | from . import torch_utils 4 | 5 | 6 | __all__ = ['init_particles', 'resampling', 'torch_utils'] -------------------------------------------------------------------------------- /docs/_templates/classtemplate.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :members: 7 | :show-inheritance: 8 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.dynamics.CBO.rst: -------------------------------------------------------------------------------- 1 | cbx.dynamics.CBO 2 | ================ 3 | 4 | .. currentmodule:: cbx.dynamics 5 | 6 | .. autoclass:: CBO 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.dynamics.CBS.rst: -------------------------------------------------------------------------------- 1 | cbx.dynamics.CBS 2 | ================ 3 | 4 | .. currentmodule:: cbx.dynamics 5 | 6 | .. autoclass:: CBS 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.dynamics.PSO.rst: -------------------------------------------------------------------------------- 1 | cbx.dynamics.PSO 2 | ================ 3 | 4 | .. currentmodule:: cbx.dynamics 5 | 6 | .. autoclass:: PSO 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :caption: Contents 7 | 8 | dynamic 9 | scheduler 10 | plotting 11 | utils 12 | objectives 13 | -------------------------------------------------------------------------------- /docs/api/generated/cbx.dynamics.PolarCBO.rst: -------------------------------------------------------------------------------- 1 | cbx.dynamics.PolarCBO 2 | ===================== 3 | 4 | .. currentmodule:: cbx.dynamics 5 | 6 | .. autoclass:: PolarCBO 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.Ackley.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Ackley 2 | ===================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Ackley 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.dynamics.CBOMemory.rst: -------------------------------------------------------------------------------- 1 | cbx.dynamics.CBOMemory 2 | ====================== 3 | 4 | .. currentmodule:: cbx.dynamics 5 | 6 | .. autoclass:: CBOMemory 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.scheduler.multiply.rst: -------------------------------------------------------------------------------- 1 | cbx.scheduler.multiply 2 | ====================== 3 | 4 | .. currentmodule:: cbx.scheduler 5 | 6 | .. autoclass:: multiply 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Ackley.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Ackley 2 | ===================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Ackley 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Bukin6.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Bukin6 2 | ===================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Bukin6 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Easom.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Easom 2 | ==================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Easom 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.dynamics.CBXDynamic.rst: -------------------------------------------------------------------------------- 1 | cbx.dynamics.CBXDynamic 2 | ======================= 3 | 4 | .. currentmodule:: cbx.dynamics 5 | 6 | .. autoclass:: CBXDynamic 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.scheduler.scheduler.rst: -------------------------------------------------------------------------------- 1 | cbx.scheduler.scheduler 2 | ======================= 3 | 4 | .. currentmodule:: cbx.scheduler 5 | 6 | .. autoclass:: scheduler 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.history.track.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.history.track 2 | ======================= 3 | 4 | .. currentmodule:: cbx.utils.history 5 | 6 | .. autoclass:: track 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.Himmelblau.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Himmelblau 2 | ========================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Himmelblau 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.McCormick.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.McCormick 2 | ======================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: McCormick 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.Rastrigin.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Rastrigin 2 | ======================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Rastrigin 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.Rosenbrock.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Rosenbrock 2 | ========================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Rosenbrock 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.snowflake.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.snowflake 2 | ======================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: snowflake 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.plotting.plot_dynamic.rst: -------------------------------------------------------------------------------- 1 | cbx.plotting.plot\_dynamic 2 | ========================== 3 | 4 | .. currentmodule:: cbx.plotting 5 | 6 | .. autoclass:: plot_dynamic 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.history.track_x.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.history.track\_x 2 | ========================== 3 | 4 | .. currentmodule:: cbx.utils.history 5 | 6 | .. autoclass:: track_x 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.McCormick.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.McCormick 2 | ======================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: McCormick 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Rastrigin.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Rastrigin 2 | ======================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Rastrigin 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.drop_wave.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.drop\_wave 2 | ========================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: drop_wave 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.eggholder.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.eggholder 2 | ======================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: eggholder 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.snowflake.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.snowflake 2 | ======================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: snowflake 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.scheduler.param_update.rst: -------------------------------------------------------------------------------- 1 | cbx.scheduler.param\_update 2 | =========================== 3 | 4 | .. currentmodule:: cbx.scheduler 5 | 6 | .. autoclass:: param_update 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Himmelblau.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Himmelblau 2 | ========================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Himmelblau 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Michalewicz.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Michalewicz 2 | ========================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Michalewicz 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Rosenbrock.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Rosenbrock 2 | ========================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Rosenbrock 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.dynamics.ParticleDynamic.rst: -------------------------------------------------------------------------------- 1 | cbx.dynamics.ParticleDynamic 2 | ============================ 3 | 4 | .. currentmodule:: cbx.dynamics 5 | 6 | .. autoclass:: ParticleDynamic 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.history.track_drift.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.history.track\_drift 2 | ============================== 3 | 4 | .. currentmodule:: cbx.utils.history 5 | 6 | .. autoclass:: track_drift 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Holder_table.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Holder\_table 2 | ============================ 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Holder_table 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.history.track_energy.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.history.track\_energy 2 | =============================== 3 | 4 | .. currentmodule:: cbx.utils.history 5 | 6 | .. autoclass:: track_energy 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.resampling.resampling.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.resampling.resampling 2 | =============================== 3 | 4 | .. currentmodule:: cbx.utils.resampling 5 | 6 | .. autoclass:: resampling 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.cross_in_tray.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.cross\_in\_tray 2 | ============================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: cross_in_tray 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.Ackley_multimodal.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Ackley\_multimodal 2 | ================================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Ackley_multimodal 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.three_hump_camel.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.three\_hump\_camel 2 | ================================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: three_hump_camel 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.history.track_consensus.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.history.track\_consensus 2 | ================================== 3 | 4 | .. currentmodule:: cbx.utils.history 5 | 6 | .. autoclass:: track_consensus 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.three_hump_camel.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.three\_hump\_camel 2 | ================================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: three_hump_camel 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.plotting.plot_dynamic_history.rst: -------------------------------------------------------------------------------- 1 | cbx.plotting.plot\_dynamic\_history 2 | =================================== 3 | 4 | .. currentmodule:: cbx.plotting 5 | 6 | .. autoclass:: plot_dynamic_history 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.history.track_drift_mean.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.history.track\_drift\_mean 2 | ==================================== 3 | 4 | .. currentmodule:: cbx.utils.history 5 | 6 | .. autoclass:: track_drift_mean 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.termination.max_it_term.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.termination.max\_it\_term 2 | =================================== 3 | 4 | .. currentmodule:: cbx.utils.termination 5 | 6 | .. autoclass:: max_it_term 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Ackley_multimodal.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Ackley\_multimodal 2 | ================================= 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Ackley_multimodal 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.objectives.Rastrigin_multimodal.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Rastrigin\_multimodal 2 | ==================================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Rastrigin_multimodal 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.scheduler.effective_sample_size.rst: -------------------------------------------------------------------------------- 1 | cbx.scheduler.effective\_sample\_size 2 | ===================================== 3 | 4 | .. currentmodule:: cbx.scheduler 5 | 6 | .. autoclass:: effective_sample_size 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.history.track_update_norm.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.history.track\_update\_norm 2 | ===================================== 3 | 4 | .. currentmodule:: cbx.utils.history 5 | 6 | .. autoclass:: track_update_norm 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.termination.diff_tol_term.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.termination.diff\_tol\_term 2 | ===================================== 3 | 4 | .. currentmodule:: cbx.utils.termination 5 | 6 | .. autoclass:: diff_tol_term 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.termination.max_eval_term.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.termination.max\_eval\_term 2 | ===================================== 3 | 4 | .. currentmodule:: cbx.utils.termination 5 | 6 | .. autoclass:: max_eval_term 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.termination.max_time_term.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.termination.max\_time\_term 2 | ===================================== 3 | 4 | .. currentmodule:: cbx.utils.termination 5 | 6 | .. autoclass:: max_time_term 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/userguide/generated/cbx.objectives.Rastrigin_multimodal.rst: -------------------------------------------------------------------------------- 1 | cbx.objectives.Rastrigin\_multimodal 2 | ==================================== 3 | 4 | .. currentmodule:: cbx.objectives 5 | 6 | .. autoclass:: Rastrigin_multimodal 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.termination.energy_tol_term.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.termination.energy\_tol\_term 2 | ======================================= 3 | 4 | .. currentmodule:: cbx.utils.termination 5 | 6 | .. autoclass:: energy_tol_term 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.particle_init.init_particles.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.particle\_init.init\_particles 2 | ======================================== 3 | 4 | .. currentmodule:: cbx.utils.particle_init 5 | 6 | .. autoclass:: init_particles 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.objective_handling.cbx_objective.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.objective\_handling.cbx\_objective 2 | ============================================ 3 | 4 | .. currentmodule:: cbx.utils.objective_handling 5 | 6 | .. autoclass:: cbx_objective 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.resampling.loss_update_resampling.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.resampling.loss\_update\_resampling 2 | ============================================= 3 | 4 | .. currentmodule:: cbx.utils.resampling 5 | 6 | .. autoclass:: loss_update_resampling 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /cbx/__init__.py: -------------------------------------------------------------------------------- 1 | __path__ = __import__('pkgutil').extend_path(__path__, __name__) 2 | 3 | from cbx import dynamics 4 | from cbx import objectives 5 | from cbx.utils import particle_init 6 | from cbx import scheduler 7 | from cbx import constraints 8 | 9 | __all__ = ["dynamics", "noise", "objectives", "particle_init", "scheduler", "constraints"] -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.objective_handling.cbx_objective_f1D.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.objective\_handling.cbx\_objective\_f1D 2 | ================================================= 3 | 4 | .. currentmodule:: cbx.utils.objective_handling 5 | 6 | .. autoclass:: cbx_objective_f1D 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.objective_handling.cbx_objective_f2D.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.objective\_handling.cbx\_objective\_f2D 2 | ================================================= 3 | 4 | .. currentmodule:: cbx.utils.objective_handling 5 | 6 | .. autoclass:: cbx_objective_f2D 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.objective_handling.cbx_objective_fh.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.objective\_handling.cbx\_objective\_fh 2 | ================================================ 3 | 4 | .. currentmodule:: cbx.utils.objective_handling 5 | 6 | .. autoclass:: cbx_objective_fh 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/api/generated/cbx.utils.resampling.ensemble_update_resampling.rst: -------------------------------------------------------------------------------- 1 | cbx.utils.resampling.ensemble\_update\_resampling 2 | ================================================= 3 | 4 | .. currentmodule:: cbx.utils.resampling 5 | 6 | .. autoclass:: ensemble_update_resampling 7 | :members: 8 | :show-inheritance: 9 | :special-members: __call__ -------------------------------------------------------------------------------- /docs/examples/JOSS/initial.txt: -------------------------------------------------------------------------------- 1 | -1.003679049221100072e+00 3.605714451279329325e+00 2 | 1.855951534491240729e+00 7.892678735762928000e-01 3 | -3.751850876460507855e+00 -2.752043837310378827e+00 4 | -3.535331102654404312e+00 2.929409166199481440e+00 5 | 8.089200939456704376e-01 1.664580622368363905e+00 6 | 3.835324045633580425e+00 -0.759278817295954589e+00 7 | -------------------------------------------------------------------------------- /docs/examples/time_benchmarks/profiling.py: -------------------------------------------------------------------------------- 1 | import cbx 2 | from cbx.dynamics import CBO 3 | import numpy as np 4 | import cProfile 5 | 6 | 7 | f = cbx.objectives.Quadratic() 8 | x = np.random.uniform(-3,3, (10,100,200)) 9 | 10 | dyn = CBO(f, x=x, max_it=300, noise='anisotropic', verbosity=0, f_dim='3D') 11 | cProfile.run('dyn.optimize()') 12 | 13 | -------------------------------------------------------------------------------- /docs/api/plotting.rst: -------------------------------------------------------------------------------- 1 | plotting 2 | ========== 3 | 4 | Provides plotting functions 5 | 6 | Classes 7 | ------- 8 | 9 | .. currentmodule:: cbx 10 | 11 | 12 | .. autosummary:: 13 | :toctree: generated 14 | :nosignatures: 15 | :recursive: 16 | :template: classtemplate.rst 17 | 18 | plotting.plot_dynamic 19 | plotting.plot_dynamic_history -------------------------------------------------------------------------------- /docs/userguide/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | -------- 3 | 4 | .. nbgallery:: 5 | 6 | ../examples/nns/mnist.ipynb 7 | ../examples/simple_example.ipynb 8 | ../examples/custom_noise.ipynb 9 | ../examples/polarcbo.ipynb 10 | ../examples/onedim_example.ipynb 11 | ../examples/low_level.ipynb 12 | ../examples/sampling.ipynb 13 | ../examples/success_evaluation.ipynb -------------------------------------------------------------------------------- /docs/api/scheduler.rst: -------------------------------------------------------------------------------- 1 | scheduler 2 | ========== 3 | 4 | This module implements schedulers for consensus schemes. 5 | 6 | Classes 7 | ------- 8 | 9 | .. currentmodule:: cbx.scheduler 10 | 11 | 12 | .. autosummary:: 13 | :toctree: generated 14 | :nosignatures: 15 | :recursive: 16 | :template: classtemplate.rst 17 | 18 | param_update 19 | scheduler 20 | multiply 21 | effective_sample_size -------------------------------------------------------------------------------- /tests/dynamics/test_pso.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics import PSO 2 | import pytest 3 | from test_abstraction import test_abstract_dynamic 4 | 5 | class Test_pso(test_abstract_dynamic): 6 | 7 | @pytest.fixture 8 | def dynamic(self): 9 | return PSO 10 | 11 | def test_step_eval(self, f, dynamic): 12 | dyn = dynamic(f, d=5, M=7, N=5) 13 | dyn.step() 14 | assert dyn.it == 1 -------------------------------------------------------------------------------- /tests/scheduler/test_effective_sample_size.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from cbx.scheduler import eff_sample_size_gap, bisection_solve 3 | 4 | 5 | def test_alpha_to_zero(): 6 | '''Test if effective sample size scheduler updates params correctly''' 7 | energy = np.random.normal((6,5,7)) 8 | gap = eff_sample_size_gap(energy, 1.) 9 | alpha = bisection_solve(gap, 0*np.ones(6,), 100*np.ones(6,), max_it = 100, thresh = 1e-6) 10 | assert np.max(alpha) < 1e-1 -------------------------------------------------------------------------------- /docs/api/objectives.rst: -------------------------------------------------------------------------------- 1 | objectives 2 | ========== 3 | 4 | This module implements different object function which can be used to test the performance of the optimization algorithms. 5 | 6 | Classes 7 | ------- 8 | 9 | .. currentmodule:: cbx 10 | 11 | .. autosummary:: 12 | :toctree: generated 13 | :nosignatures: 14 | :recursive: 15 | :template: classtemplate.rst 16 | 17 | objectives.objective 18 | objectives.Himmelblau 19 | objectives.Rosenbrock 20 | objectives.three_hump_camel 21 | objectives.McCormick 22 | objectives.Ackley 23 | objectives.Ackley_multimodal 24 | objectives.Rastrigin 25 | objectives.Rastrigin_multimodal 26 | objectives.snowflake 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. If applicable, please include a code example to reproduce the bug. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. iOS] 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /cbx/dynamics/__init__.py: -------------------------------------------------------------------------------- 1 | from .pdyn import ParticleDynamic, CBXDynamic 2 | from .cbo import CBO 3 | from .cbo_memory import CBOMemory 4 | from .adamcbo import AdamCBO 5 | from .pso import PSO 6 | from .cbs import CBS 7 | from .polarcbo import PolarCBO 8 | from .mirrorcbo import MirrorCBO 9 | from .driftconstrainedcbo import DriftConstrainedCBO 10 | from .hypersurfacecbo import HyperSurfaceCBO 11 | from .regcombinationcbo import RegCombinationCBO 12 | 13 | __all__ = ['ParticleDynamic', 14 | 'CBXDynamic', 15 | 'CBO', 16 | 'CBOMemory', 17 | 'AdamCBO', 18 | 'PSO', 19 | 'CBS', 20 | 'PolarCBO', 21 | 'MirrorCBO', 22 | 'DriftConstrainedCBO', 23 | 'HyperSurfaceCBO', 24 | 'RegCombinationCBO'] 25 | 26 | -------------------------------------------------------------------------------- /docs/userguide/objectives.rst: -------------------------------------------------------------------------------- 1 | Test Objectives 2 | =============== 3 | 4 | The following objective functions can be used to test optimization algorithms. 5 | 6 | Classes 7 | ------- 8 | 9 | .. currentmodule:: cbx 10 | 11 | .. autosummary:: 12 | :toctree: generated 13 | :nosignatures: 14 | :recursive: 15 | :template: classtemplate.rst 16 | 17 | objectives.Ackley 18 | objectives.Ackley_multimodal 19 | objectives.Bukin6 20 | objectives.cross_in_tray 21 | objectives.drop_wave 22 | objectives.Easom 23 | objectives.eggholder 24 | objectives.Himmelblau 25 | objectives.Holder_table 26 | objectives.McCormick 27 | objectives.Michalewicz 28 | objectives.Rastrigin 29 | objectives.Rastrigin_multimodal 30 | objectives.Rosenbrock 31 | objectives.snowflake 32 | objectives.three_hump_camel 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 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 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 | -------------------------------------------------------------------------------- /tests/dynamics/test_adam_cbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics.adamcbo import AdamCBO 2 | from test_cbo import Test_cbo 3 | import pytest 4 | 5 | 6 | class Test_adamcbo(Test_cbo): 7 | """ 8 | Test class for AdamCBO dynamic. 9 | Inherits from Test_cbo to reuse tests for CBO dynamics. 10 | """ 11 | 12 | @pytest.fixture 13 | def dynamic(self): 14 | return AdamCBO 15 | 16 | def test_step(self, dynamic, f): 17 | # ToDo: Implement a test for the step function 18 | pass 19 | 20 | def test_step_batched(self, dynamic, f): 21 | # ToDo: Implement a test for the step function with batching 22 | pass 23 | 24 | def test_step_batched_partial(self, dynamic, f): 25 | # ToDo: Implement a test for the step function with batching 26 | pass 27 | 28 | def test_torch_handling(self, f, dynamic): 29 | # ToDo: Implement a test for handling torch tensors 30 | pass -------------------------------------------------------------------------------- /.github/workflows/Sphinx.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Docs 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - docs/** 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | - name: Install dependencies 15 | run: | 16 | sudo apt-get install pandoc 17 | pip install sphinx sphinx_rtd_theme 18 | pip install sphinx_design 19 | pip install nbsphinx 20 | pip install pydata-sphinx-theme 21 | pip install -r requirements.txt 22 | - name: Sphinx build 23 | run: | 24 | sphinx-build docs _build 25 | - name: Deploy 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | publish_branch: gh-pages 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: _build/ 31 | force_orphan: true 32 | -------------------------------------------------------------------------------- /tests/scheduler/test_scheduler.py: -------------------------------------------------------------------------------- 1 | import cbx 2 | from cbx.scheduler import multiply, scheduler 3 | 4 | def test_multiply_update(): 5 | '''Test if multiply scheduler updates params correctly''' 6 | dyn = cbx.dynamics.CBO(f=lambda x: x**2, d=1, max_it=1, alpha=1.0, sigma=1.0) 7 | sched = scheduler([multiply(name='alpha', factor=1.5), 8 | multiply(name='sigma', factor=2.0)]) 9 | 10 | dyn.optimize(sched=sched) 11 | assert dyn.alpha == 1.5 12 | assert dyn.sigma == 2.0 13 | 14 | def test_multiply_maximum(): 15 | '''Test if multiply scheduler respects maximum''' 16 | dyn = cbx.dynamics.CBO(f=lambda x: x**2, d=1, max_it=10, alpha=1.0, sigma=1.0) 17 | sched = scheduler([multiply(name='alpha', factor=1.5, maximum=2.0), 18 | multiply(name='sigma', factor=2.0, maximum=4.7)]) 19 | 20 | dyn.optimize(sched=sched) 21 | assert dyn.alpha == 2.0 22 | assert dyn.sigma == 4.7 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/api/dynamic.rst: -------------------------------------------------------------------------------- 1 | dynamics 2 | ======== 3 | 4 | This module implements algorithms for optimization and sampling for consensus based 5 | particles systems as proposed in [1]_. 6 | 7 | The base class ``ParticleDynamic`` implements functionality that is common 8 | to particle based iterative methods. The class ``CBXDynamic`` inherits from 9 | ``ParticleDynamic`` and implements functionality that is specific to consensus 10 | based schemes. The following dynamics are implemented: 11 | 12 | .. currentmodule:: cbx.dynamics 13 | 14 | 15 | .. autosummary:: 16 | :toctree: generated 17 | :nosignatures: 18 | :recursive: 19 | :template: classtemplate.rst 20 | 21 | ParticleDynamic 22 | CBXDynamic 23 | CBO 24 | CBOMemory 25 | CBS 26 | PSO 27 | PolarCBO 28 | 29 | 30 | 31 | 32 | References 33 | ---------- 34 | 35 | .. [1] Pinnau, R., Totzeck, C., Tse, O., & Martin, S. (2017). A consensus-based model for global optimization and its mean-field limit. Mathematical Models and Methods in Applied Sciences, 27(01), 183-204. 36 | -------------------------------------------------------------------------------- /docs/examples/nns/models.py: -------------------------------------------------------------------------------- 1 | import torch.nn as nn 2 | 3 | class Perceptron(nn.Module): 4 | def __init__(self, mean = 0.0, std = 1.0, 5 | act_fun=nn.ReLU, 6 | sizes = None): 7 | super(Perceptron, self).__init__() 8 | # 9 | self.mean = mean 10 | self.std = std 11 | self.act_fun = act_fun() 12 | self.sizes = sizes if sizes else [784, 10] 13 | self.linears = nn.ModuleList([nn.Linear(self.sizes[i], self.sizes[i+1]) for i in range(len(self.sizes)-1)]) 14 | self.bns = nn.ModuleList([nn.BatchNorm1d(self.sizes[i+1], track_running_stats=False) for i in range(len(self.sizes)-1)]) 15 | self.sm = nn.Softmax(dim=1) 16 | 17 | def __call__(self, x): 18 | x = x.view([x.shape[0], -1]) 19 | x = (x - self.mean)/self.std 20 | 21 | for linear, bn in zip(self.linears, self.bns): 22 | x = linear(x) 23 | x = self.act_fun(x) 24 | x = bn(x) 25 | 26 | # apply softmax 27 | x = self.sm(x) 28 | return x -------------------------------------------------------------------------------- /tests/dynamics/test_cbo_memory.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics import CBOMemory 2 | import pytest 3 | from test_abstraction import test_abstract_dynamic 4 | import numpy as np 5 | 6 | class Test_cbo_memory(test_abstract_dynamic): 7 | 8 | @pytest.fixture 9 | def dynamic(self): 10 | return CBOMemory 11 | 12 | def test_step_eval(self, f, dynamic): 13 | dyn = dynamic(f, d=5, M=7, N=5, max_it=1) 14 | dyn.step() 15 | assert dyn.it == 1 16 | 17 | def test_update_best_cur_particle(self, f, dynamic): 18 | x = np.zeros((5,3,2)) 19 | x[0, :,:] = np.array([[0.,0.], [2.,1.], [4.,5.]]) 20 | x[1, :,:] = np.array([[8.,7.], [0.,1.], [2.,1.]]) 21 | x[2, :,:] = np.array([[2.,5.], [0.,0.5], [2.,1.]]) 22 | x[3, :,:] = np.array([[5.3,0.], [2.,1.], [0.,0.3]]) 23 | x[4, :,:] = np.array([[0.,3.], [2.,1.], [0.,1.]]) 24 | dyn = dynamic(f, x=x) 25 | dyn.update_best_cur_particle() 26 | best_cur_particle = np.array([[0.,0.], [0.,1.], [0.,0.5], [0.,0.3], [0.,1.]]) 27 | 28 | assert np.allclose(dyn.best_cur_particle, best_cur_particle) -------------------------------------------------------------------------------- /tests/dynamics/test_hypersurface_cbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics.hypersurfacecbo import HyperSurfaceCBO 2 | from test_cbo import Test_cbo 3 | import pytest 4 | 5 | 6 | class Test_hypersurface_cbo(Test_cbo): 7 | """ 8 | Test class for AdamCBO dynamic. 9 | Inherits from Test_cbo to reuse tests for CBO dynamics. 10 | """ 11 | 12 | @pytest.fixture 13 | def dynamic(self): 14 | return HyperSurfaceCBO 15 | 16 | def test_step(self, dynamic, f): 17 | # ToDo: Implement a test for the step function 18 | pass 19 | 20 | def test_step_batched(self, dynamic, f): 21 | # ToDo: Implement a test for the step function with batching 22 | pass 23 | 24 | def test_step_batched_partial(self, dynamic, f): 25 | # ToDo: Implement a test for the step function with batching 26 | pass 27 | 28 | def test_torch_handling(self, f, dynamic): 29 | # ToDo: Implement a test for handling torch tensors 30 | pass 31 | 32 | def test_optimization_performance(self, f, dynamic, opt_kwargs): 33 | # ToDo: Implement a test for optimization performance 34 | pass -------------------------------------------------------------------------------- /tests/dynamics/test_driftconstrained_cbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics.driftconstrainedcbo import DriftConstrainedCBO 2 | from test_cbo import Test_cbo 3 | import pytest 4 | 5 | 6 | class Test_driftconstrained_cbo(Test_cbo): 7 | """ 8 | Test class for AdamCBO dynamic. 9 | Inherits from Test_cbo to reuse tests for CBO dynamics. 10 | """ 11 | 12 | @pytest.fixture 13 | def dynamic(self): 14 | return DriftConstrainedCBO 15 | 16 | def test_step(self, dynamic, f): 17 | # ToDo: Implement a test for the step function 18 | pass 19 | 20 | def test_step_batched(self, dynamic, f): 21 | # ToDo: Implement a test for the step function with batching 22 | pass 23 | 24 | def test_step_batched_partial(self, dynamic, f): 25 | # ToDo: Implement a test for the step function with batching 26 | pass 27 | 28 | def test_torch_handling(self, f, dynamic): 29 | # ToDo: Implement a test for handling torch tensors 30 | pass 31 | 32 | def test_optimization_performance(self, f, dynamic, opt_kwargs): 33 | # ToDo: Implement a test for optimization performance 34 | pass -------------------------------------------------------------------------------- /tests/dynamics/test_regcombination_cbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics.regcombinationcbo import RegCombinationCBO 2 | from test_cbo import Test_cbo 3 | import pytest 4 | 5 | 6 | class Test_regcombination_cbo(Test_cbo): 7 | """ 8 | Test class for RegCombination dynamic. 9 | Inherits from Test_cbo to reuse tests for CBO dynamics. 10 | """ 11 | 12 | @pytest.fixture 13 | def dynamic(self): 14 | return RegCombinationCBO 15 | 16 | def test_step(self, dynamic, f): 17 | # ToDo: Implement a test for the step function 18 | pass 19 | 20 | def test_step_batched(self, dynamic, f): 21 | # ToDo: Implement a test for the step function with batching 22 | pass 23 | 24 | def test_step_batched_partial(self, dynamic, f): 25 | # ToDo: Implement a test for the step function with batching 26 | pass 27 | 28 | def test_torch_handling(self, f, dynamic): 29 | # ToDo: Implement a test for handling torch tensors 30 | pass 31 | 32 | def test_optimization_performance(self, f, dynamic, opt_kwargs): 33 | # ToDo: Implement a test for optimization performance 34 | pass -------------------------------------------------------------------------------- /tests/utils/test_objective_handeling.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from cbx.utils.objective_handling import _promote_objective 4 | 5 | def test_f_dim_1D_handeling(): 6 | '''Test if f_dim is correctly handeled for 1D''' 7 | def f(x): return np.sum(x**2) 8 | f_dim = '1D' 9 | f_promote = _promote_objective(f, f_dim) 10 | x = np.random.uniform(-1,1,(6,5,7)) 11 | res = np.array([f(x[i,j,:]) for i in range(6) for j in range(5)]).reshape(6,5) 12 | 13 | assert np.all(f_promote(x) == res) 14 | 15 | def test_f_dim_2D_handeling(): 16 | '''Test if f_dim is correctly handeled for 2D''' 17 | def f(x): return np.sum(x**2, axis=-1) 18 | f_dim = '2D' 19 | f_promote = _promote_objective(f, f_dim) 20 | x = np.random.uniform(-1,1,(6,5,7)) 21 | res = np.array([f(x[i,j,:]) for i in range(6) for j in range(5)]).reshape(6,5) 22 | 23 | assert np.all(f_promote(x) == res) 24 | 25 | def test_f_dim_unknown(): 26 | '''Test if f_dim raises error for unknown f_dim''' 27 | def f(x): return np.sum(x**2) 28 | f_dim = '4D' 29 | 30 | with pytest.raises(ValueError): 31 | _promote_objective(f, f_dim) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Purpose-driven particle systems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/userguide/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | CBX implements consensus based particle schemes. Below, we list the pages that explain the main functionality and modules. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | dynamics 10 | sched 11 | objectives 12 | plotting 13 | examples 14 | 15 | Basic Parameters and nomenclature 16 | --------------------------------- 17 | 18 | We collect some basic parameters and terminology that are used in the following 19 | 20 | ============== ============================================== 21 | **symbol** **description** 22 | ============== ============================================== 23 | :math:`x` The ensemble of points 24 | :math:`f` The objective function 25 | :math:`M` The number of runs 26 | :math:`N` The number of particles 27 | :math:`d` The dimension 28 | :math:`dt` The time step parameter 29 | :math:`t` The current time 30 | :math:`\alpha` The weighting parameter for the consensus point 31 | :math:`\sigma` The noise scaling parameter 32 | ============== ============================================== 33 | -------------------------------------------------------------------------------- /tests/utils/test_particle_init.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from cbx.utils.particle_init import init_particles 4 | 5 | def test_particle_init_uniform(): 6 | '''Test if particles are correctly initialized''' 7 | shape = (2,3,5,7) 8 | x = init_particles(shape=shape, x_min=-1, x_max=1, method="uniform") 9 | assert x.shape == (2,3,5,7) 10 | assert np.all(x >= -1) 11 | assert np.all(x <= 1) 12 | 13 | @pytest.mark.parametrize("in_shape, out_shape", [((3,5,7), (3,5,7)), ((5,7), (1,5,7))]) 14 | def test_particle_init_normal(in_shape, out_shape): 15 | '''Test if particles are correctly initialized''' 16 | x = init_particles(shape=in_shape, method="normal") 17 | assert x.shape == out_shape 18 | 19 | def test_particle_init_normal_wrong_shape(): 20 | '''Test if exception is raised when normal initialization is used for 2D particles''' 21 | with pytest.raises(RuntimeError): 22 | init_particles(shape=(3,), method="normal") 23 | 24 | def test_particle_init_unknown_method(): 25 | '''Test if exception is raised when unknown method is specified''' 26 | with pytest.raises(RuntimeError): 27 | init_particles(shape=(3,5,7), method="unknown") 28 | -------------------------------------------------------------------------------- /.github/workflows/Tests.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10", "3.11"] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install ruff pytest 23 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 24 | - name: Lint with ruff 25 | run: | 26 | # stop the build if there are Python syntax errors or undefined names 27 | ruff check . --output-format=github --select=E9,F63,F7,F82 --target-version=py37 . 28 | # default set of ruff rules with GitHub Annotations 29 | ruff check . --output-format=github --target-version=py37 . 30 | - name: Install package 31 | run: | 32 | pip install --upgrade build 33 | pip install -e .[test] 34 | - name: Test with pytest 35 | run: | 36 | pytest 37 | -------------------------------------------------------------------------------- /tests/dynamics/test_cbs.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics import CBS 2 | import pytest 3 | from test_abstraction import test_abstract_dynamic 4 | import numpy as np 5 | 6 | class Test_CBS(test_abstract_dynamic): 7 | 8 | @pytest.fixture 9 | def opt_kwargs(self): 10 | return{'d':2, 'M':5, 'N':50, 'max_it':50, 'check_f_dims':False, 11 | 'alpha':0.5, 'mode':'optimization'} 12 | 13 | @pytest.fixture 14 | def dynamic(self): 15 | return CBS 16 | 17 | def test_step_eval(self, f, dynamic): 18 | dyn = dynamic(f, d=5, M=7, N=5, max_it=1) 19 | dyn.step() 20 | assert dyn.it == 1 21 | 22 | def test_run(self, f, dynamic): 23 | dyn = dynamic(f, d=5, M=7, N=5, max_it=3) 24 | dyn.run() 25 | assert dyn.it == 3 26 | 27 | def test_run_optimization(self, f, dynamic): 28 | dyn = dynamic(f, d=5, M=7, N=5, max_it=2, mode='optimization') 29 | dyn.run() 30 | assert dyn.it == 2 31 | 32 | def test_multi_dim_domain(self, f, dynamic): 33 | x = np.ones((5,7,2,3,1)) 34 | def g(x): 35 | return np.sum(x, axis=(2,3,4))**2 36 | 37 | with pytest.raises(NotImplementedError): 38 | dynamic(g, x=x, f_dim ='3D') -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name="cbx" 7 | version="1.0.1" 8 | authors = [ 9 | {name = "Tim Roith", email = "tim.roith@desy.de"}, 10 | ] 11 | description="CBXpy" 12 | dependencies = [ 13 | 'numpy', 14 | 'scipy', 15 | 'matplotlib' 16 | ] 17 | readme = "README.md" 18 | requires-python = ">3.5.2" 19 | classifiers = [ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent" 23 | ] 24 | 25 | [project.optional-dependencies] # Optional dependencies 26 | test = [ 27 | 'pytest', 28 | 'torch' 29 | ] 30 | 31 | torch = [ 32 | 'torch', 33 | ] 34 | 35 | [tool.setuptools] 36 | packages = ['cbx', 'cbx.dynamics', 'cbx.utils'] 37 | 38 | 39 | [tool.ruff] 40 | # Enable flake8-bugbear (`B`) rules. 41 | select = ["E", "F", "B"] 42 | 43 | # Never enforce `E501` (line length violations). 44 | ignore = ["E501"] 45 | 46 | # Avoid trying to fix flake8-bugbear (`B`) violations. 47 | unfixable = ["B"] 48 | 49 | # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. 50 | [tool.ruff.per-file-ignores] 51 | "__init__.py" = ["E402"] 52 | "path/to/file.py" = ["E402"] 53 | "**/{tests,docs,tools}/*" = ["E402"] 54 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: CodeCov 2 | on: workflow_dispatch 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 9 | 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Set up Python ${{ matrix.python-version }} 13 | uses: actions/setup-python@master 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 20 | - name: Install package 21 | run: | 22 | pip install --upgrade build 23 | pip install -e .[test] 24 | - name: Generate coverage report 25 | run: | 26 | pip install pytest 27 | pip install pytest-cov 28 | pytest --cov=./ --cov-report=xml 29 | ls -la 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v4 32 | with: 33 | env_vars: OS,PYTHON 34 | fail_ci_if_error: true 35 | files: ./coverage.xml 36 | flags: unittests 37 | name: codecov-umbrella 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | verbose: true 40 | -------------------------------------------------------------------------------- /tests/dynamics/test_mirror_cbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics.mirrorcbo import ( 2 | MirrorCBO, ProjectionBall, ProjectionHyperplane, 3 | mirror_dict 4 | ) 5 | from test_cbo import Test_cbo 6 | import numpy as np 7 | import pytest 8 | 9 | class Test_mirrorcbo(Test_cbo): 10 | """ 11 | Test class for MirrorCBO dynamics. 12 | Inherits from Test_cbo to reuse tests for CBO dynamics. 13 | """ 14 | 15 | @pytest.fixture 16 | def dynamic(self): 17 | return MirrorCBO 18 | 19 | def test_if_all_mirror_maps_are_loadable(self, dynamic, f): 20 | """ 21 | Test if all mirror maps in mirror_dict are loadable. 22 | """ 23 | for mm in mirror_dict.keys(): 24 | dyn = dynamic(f, d=5, M=4, N=3, mirrormap=mm) 25 | dyn.step() 26 | 27 | def test_ProjectionBall(self, dynamic, f): 28 | """ 29 | Test the ProjectionBall mirror map. 30 | """ 31 | dyn = dynamic(f, d=5, M=4, N=3, mirrormap=ProjectionBall(radius=1.0)) 32 | dyn.step() 33 | assert dyn.it > 0 34 | assert np.all(np.linalg.norm(dyn.x - dyn.mirrormap.center, axis=-1) <= 1 + 1e-5) 35 | 36 | def test_ProjectionHyperplane(self, dynamic, f): 37 | """ 38 | Test the ProjectionHyperplane mirror map. 39 | """ 40 | dyn = dynamic(f, d=5, M=4, N=3, mirrormap=ProjectionHyperplane(a=np.ones((5,)))) 41 | dyn.step() 42 | assert dyn.it > 0 43 | assert np.all(np.abs(dyn.x @ np.ones((5,))) <= 1e-5) -------------------------------------------------------------------------------- /cbx/utils/particle_init.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def init_particles(shape=(1,1,1), x_min=-1.0, x_max = 1.0, delta=1.0, method="uniform"): 4 | r"""Initialize particles 5 | 6 | Parameters 7 | ---------- 8 | N : int, optional 9 | Number of particles. The default is 100. 10 | d : int, optional 11 | Dimension of the particles. The default is 2. 12 | x_min : float, optional 13 | Lower bound for the uniform distribution. The default is 0.0. 14 | x_max : float, optional 15 | Upper bound for the uniform distribution. The default is 1.0. 16 | delta : float, optional 17 | Standard deviation for the normal distribution. The default is 1.0. 18 | method : str, optional 19 | Method for initializing the particles. The default is "uniform". 20 | Possible values: "uniform", "normal" 21 | 22 | Returns 23 | ------- 24 | x : numpy.ndarray 25 | Array of particles of shape (N, d) 26 | """ 27 | 28 | 29 | if method == "uniform": 30 | x = np.random.uniform(x_min, x_max, shape) 31 | elif method == "normal": 32 | if len(shape) == 3: 33 | M, N, d = shape 34 | elif len(shape) == 2: 35 | N, d = shape 36 | M = 1 37 | else: 38 | raise RuntimeError('Normal initialization only supported for 2D or 3D shapes!') 39 | 40 | x = np.random.multivariate_normal(np.zeros((d,)), delta * np.eye(d), (M, N)) 41 | else: 42 | raise RuntimeError('Unknown method for init_particles specified!') 43 | 44 | return x -------------------------------------------------------------------------------- /docs/userguide/sched.rst: -------------------------------------------------------------------------------- 1 | .. _sched: 2 | Schedulers 3 | ========== 4 | 5 | Schedulers allow to update parameters of the dynamic. Most commonly, one may want to adjust the heat parameter :math:`\alpha` during the iteration. 6 | The base class is :class:`cbx.scheduler.param_update` and all schedulers are derived from it. 7 | 8 | The function that performs the update is :func:`update `. The scheduler accesses the parameters of the dynamic from the outside, therefore the dynamic needs to be passed to the update function. 9 | 10 | 11 | The easiest scheduler :class:`multiply ` simply multiplies the specified value by a constant. 12 | 13 | >>> from cbx.dynamics import CBXDynamic 14 | >>> from cbx.scheduler import multiply 15 | >>> dyn = CBXDynamic(lambda x:x**2, d=1, alpha=1.0) 16 | >>> sched = multiply(name='alpha', factor=0.5) 17 | >>> sched.update(dyn) 18 | >>> print(dyn.alpha) 19 | 0.5 20 | 21 | 22 | Specifying multiple updates 23 | --------------------------- 24 | 25 | If you want to specify multiple parameter updates, you can use the class :class:`scheduler ` which allows you to pass multiple scheduler. It also implements an update function, which uses the ``update`` function of the individual schedulers. 26 | 27 | >>> from cbx.dynamics import CBXDynamic 28 | >>> from cbx.scheduler import scheduler, multiply 29 | >>> dyn = CBXDynamic(lambda x:x**2, d=1, alpha=1.0, dt=0.5) 30 | >>> sched = schedulr([multiply(name='dt', factor=0.5), multiply(name='alpha', factor=0.5)]) 31 | >>> sched.update(dyn) 32 | >>> print(dyn.alpha) 33 | >>> print(dyn.dt) 34 | 0.5 35 | 0.25 36 | 37 | -------------------------------------------------------------------------------- /cbx/dynamics/regcombinationcbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics import CBO 2 | from cbx.dynamics.pdyn import post_process_default 3 | from cbx.constraints import get_constraints, MultiConstraint 4 | from cbx.regularizers import regularize_objective 5 | 6 | class RegCombinationCBO(CBO): 7 | ''' 8 | Implements the algorithm in [1] 9 | 10 | 11 | [1] Carrillo, José A., et al. 12 | "Carrillo, J. A., Totzeck, C., & Vaes, U. (2023). 13 | Consensus-based optimization and ensemble kalman inversion for 14 | global optimization problems with constraints" 15 | arXiv preprint https://arxiv.org/abs/2111.02970 (2024). 16 | ''' 17 | 18 | def __init__(self, f, constraints = None, eps=0.01, nu = 1, **kwargs): 19 | super().__init__(f, **kwargs) 20 | self.G = MultiConstraint(get_constraints(constraints)) 21 | self.eps = eps 22 | self.f = regularize_objective(self.f, self.G.squared, lamda=1/nu) 23 | 24 | def inner_step(self, ): 25 | self.compute_consensus() 26 | self.drift = self.x - self.consensus 27 | noise = self.sigma * self.noise() 28 | 29 | # update particle positions 30 | 31 | x_tilde = self.x - self.lamda * self.dt * self.drift + noise 32 | if self.eps < 1e15: 33 | self.x = self.G.solve_Id_plus_call_grad( 34 | self.x, 35 | x_tilde, 36 | factor = self.dt / self.eps 37 | ) 38 | #self.x = solve_system(A, x_tilde) 39 | else: 40 | self.x = x_tilde 41 | 42 | def set_post_process(self, post_process): 43 | self.post_process = ( 44 | post_process if post_process is not None else post_process_default(copy_nan_to_num=True) 45 | ) -------------------------------------------------------------------------------- /docs/api/utils.rst: -------------------------------------------------------------------------------- 1 | .. _utils: 2 | utils 3 | ========== 4 | 5 | This module implements some helpful utilities. 6 | 7 | Termination criteria 8 | --------------------- 9 | 10 | .. currentmodule:: cbx.utils 11 | 12 | .. autosummary:: 13 | :toctree: generated 14 | :nosignatures: 15 | :recursive: 16 | :template: classtemplate.rst 17 | 18 | termination.energy_tol_term 19 | termination.diff_tol_term 20 | termination.max_eval_term 21 | termination.max_it_term 22 | termination.max_time_term 23 | 24 | Resampling schemes 25 | ------------------ 26 | 27 | .. currentmodule:: cbx.utils 28 | 29 | .. autosummary:: 30 | :toctree: generated 31 | :nosignatures: 32 | :recursive: 33 | :template: classtemplate.rst 34 | 35 | resampling.resampling 36 | resampling.ensemble_update_resampling 37 | resampling.loss_update_resampling 38 | 39 | 40 | 41 | History 42 | ------- 43 | 44 | .. currentmodule:: cbx.utils 45 | 46 | .. autosummary:: 47 | :toctree: generated 48 | :nosignatures: 49 | :recursive: 50 | :template: classtemplate.rst 51 | 52 | history.track 53 | history.track_x 54 | history.track_energy 55 | history.track_update_norm 56 | history.track_consensus 57 | history.track_drift 58 | history.track_drift_mean 59 | 60 | 61 | Particle initialization 62 | ------------------------ 63 | 64 | .. currentmodule:: cbx.utils 65 | 66 | .. autosummary:: 67 | :toctree: generated 68 | :nosignatures: 69 | :recursive: 70 | :template: classtemplate.rst 71 | 72 | particle_init.init_particles 73 | 74 | Objective Handling 75 | ------------------ 76 | 77 | .. currentmodule:: cbx.utils 78 | 79 | .. autosummary:: 80 | :toctree: generated 81 | :nosignatures: 82 | :recursive: 83 | :template: classtemplate.rst 84 | 85 | objective_handling.cbx_objective 86 | objective_handling.cbx_objective_fh 87 | objective_handling.cbx_objective_f1D 88 | objective_handling.cbx_objective_f2D 89 | -------------------------------------------------------------------------------- /docs/examples/sampling.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "c5befff4-2454-4e4d-989b-df4896d92c7a", 6 | "metadata": {}, 7 | "source": [ 8 | "# Consensus Based Sampling" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "4be3d815-ec76-4499-98a5-062e6428796a", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "%load_ext autoreload\n", 19 | "%autoreload 2\n", 20 | "from cbx.dynamics import CBS\n", 21 | "from cbx.objectives import Quadratic\n", 22 | "from cbx.plotting import PlotDynamicHistory\n", 23 | "f = Quadratic()\n", 24 | "dyn = CBS(f, d=5, M=3, max_it=1000, track_args={'names':['x']}, alpha=0.5)\n", 25 | "dyn.run()" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "id": "8f58f099-28ca-4365-9670-2e062fc3b878", 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "import matplotlib.pyplot as plt\n", 36 | "from IPython import display\n", 37 | "\n", 38 | "fig, ax = plt.subplots(1,)\n", 39 | "pl = PlotDynamicHistory(dyn, ax=ax,objective_args={'x_min':-3, 'x_max':3},)\n", 40 | "for i in range(0, pl.max_it,1):\n", 41 | " pl.plot_at_ind(i)\n", 42 | " pl.decorate_at_ind(i)\n", 43 | " display.display(fig)\n", 44 | " display.clear_output(wait=True)\n", 45 | " plt.pause(0.1)" 46 | ] 47 | } 48 | ], 49 | "metadata": { 50 | "kernelspec": { 51 | "display_name": "Python 3", 52 | "language": "python", 53 | "name": "python3" 54 | }, 55 | "language_info": { 56 | "codemirror_mode": { 57 | "name": "ipython", 58 | "version": 3 59 | }, 60 | "file_extension": ".py", 61 | "mimetype": "text/x-python", 62 | "name": "python", 63 | "nbconvert_exporter": "python", 64 | "pygments_lexer": "ipython3", 65 | "version": "3.12.4" 66 | } 67 | }, 68 | "nbformat": 4, 69 | "nbformat_minor": 5 70 | } 71 | -------------------------------------------------------------------------------- /docs/examples/time_benchmarks/objective.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cbx as cbx 3 | from cbx.dynamics import CBO 4 | from cbx.objectives import Quadratic 5 | from cbx.utils.objective_handling import cbx_objective_fh 6 | import timeit 7 | import jax 8 | import jax.numpy as jnp 9 | import numba 10 | 11 | np.random.seed(42) 12 | #%% 13 | conf = {'alpha': 40.0, 14 | 'dt': 0.01, 15 | 'sigma': 8.1,#8,#5.1,#8.0, 16 | 'lamda': 1.0, 17 | 'batch_args':{ 18 | 'batch_size':50, 19 | 'batch_partial': False}, 20 | 'd': 20, 21 | 'term_args':{'max_it': 1000}, 22 | 'N': 1000, 23 | 'M': 10, 24 | 'track_args': {'names': 25 | ['update_norm', 26 | 'energy','x', 27 | 'consensus', 28 | 'drift']}, 29 | 'update_thresh': 0.002} 30 | 31 | #%% Define the objective function 32 | @cbx_objective_fh 33 | def norm_dec(x): 34 | return np.linalg.norm(x, axis=-1) 35 | @jax.jit 36 | def norm_jnp(x): 37 | return jnp.linalg.norm(x, axis=-1) 38 | 39 | @numba.jit(nopython=True) 40 | def norm_numba(x): 41 | out = np.zeros(x.shape[:2]) 42 | for m in range(x.shape[0]): 43 | for n in range(x.shape[1]): 44 | out[m,n] = np.linalg.norm(x[m,n,:]) 45 | return out 46 | 47 | 48 | obs = {'CBX.objectives': Quadratic(), 49 | 'Decorated':norm_dec, 50 | 'jnp': norm_jnp, 51 | 'numba': norm_numba} 52 | #%% Define the initial positions of the particles 53 | x = cbx.utils.init_particles(shape=(conf['M'], conf['N'], conf['d']), x_min=-3., x_max = 3.) 54 | 55 | #%% Define the CBO algorithm 56 | 57 | for f in obs.keys(): 58 | dyn = CBO(obs[f], x=x, noise='anisotropic', f_dim='3D', 59 | **conf) 60 | T = timeit.Timer(dyn.step) 61 | rep = 10 62 | r = T.repeat(rep, number=50) 63 | best = min(r) 64 | print(f+ ' Best of ' +str(rep) + ': ' +str(best)[:6] + 's') 65 | 66 | -------------------------------------------------------------------------------- /tests/dynamics/test_abstraction.py: -------------------------------------------------------------------------------- 1 | import cbx 2 | import cbx.objectives as objectives 3 | import pytest 4 | import numpy as np 5 | 6 | class test_abstract_dynamic(): 7 | 8 | @pytest.fixture 9 | def dynamic(self): 10 | raise NotImplementedError() 11 | 12 | @pytest.fixture 13 | def f(self): 14 | return cbx.objectives.Quadratic() 15 | 16 | @pytest.fixture 17 | def opt_kwargs(self): 18 | return{'d':2, 'M':5, 'N':50, 'max_it':50, 'check_f_dims':False, 19 | 'alpha':50, 'sigma':1.} 20 | 21 | def test_eval_counting(self, f, dynamic): 22 | '''Test if evaluation counting is correct''' 23 | f.reset() 24 | dyn = dynamic(f, d=5, M=7, N=5, max_it=3, 25 | check_f_dims=False) 26 | dyn.optimize() 27 | 28 | assert dyn.num_f_eval.shape == (7,) 29 | assert dyn.num_f_eval.sum() == dyn.f.num_eval 30 | 31 | def test_optimization_performance(self, f, dynamic, opt_kwargs): 32 | thresh = 0.5 33 | test_funs = [objectives.Rastrigin(), objectives.Ackley(), 34 | objectives.three_hump_camel()] 35 | for g in test_funs: 36 | dyn = dynamic(g, **opt_kwargs) 37 | dyn.optimize() 38 | idx = np.argmin(dyn.best_energy) 39 | best_particle = dyn.best_particle[idx, :] 40 | assert np.linalg.norm(best_particle - g.minima) < thresh 41 | 42 | 43 | def test_step_eval(self, f, dynamic): 44 | dyn = dynamic(f, d=5, M=7, N=5, max_it=1) 45 | dyn.step() 46 | assert dyn.it == 1 47 | 48 | def test_multi_dim_domain(self, f, dynamic): 49 | x = np.ones((5,7,2,3,1)) 50 | def g(x): 51 | return np.sum(x, axis=(2,3,4))**2 52 | 53 | dyn = dynamic(g, x=x, f_dim ='3D') 54 | assert dyn.M == 5 55 | assert dyn.N == 7 56 | dyn.step() 57 | assert dyn.d == (2,3,1) -------------------------------------------------------------------------------- /docs/examples/time_benchmarks/rng.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | import cbx 3 | from cbx.dynamics import CBO 4 | import numpy as np 5 | from numpy.random import default_rng, SeedSequence 6 | import multiprocessing 7 | import concurrent.futures 8 | 9 | 10 | f = cbx.objectives.Quadratic() 11 | x = np.random.uniform(-3,3, (10,100,200)) 12 | rep = 10 13 | 14 | 15 | class MultithreadedRNG: 16 | def __init__(self, n, seed=None, threads=None): 17 | if threads is None: 18 | threads = multiprocessing.cpu_count() 19 | self.threads = threads 20 | 21 | seq = SeedSequence(seed) 22 | self._random_generators = [default_rng(s) 23 | for s in seq.spawn(threads)] 24 | 25 | self.n = n 26 | self.executor = concurrent.futures.ThreadPoolExecutor(threads) 27 | self.values = np.empty(n) 28 | self.step = np.ceil(n / threads).astype(np.int_) 29 | 30 | def fill(self): 31 | def _fill(random_state, out, first, last): 32 | random_state.standard_normal(out=out[first:last]) 33 | 34 | futures = {} 35 | for i in range(self.threads): 36 | args = (_fill, 37 | self._random_generators[i], 38 | self.values, 39 | i * self.step, 40 | (i + 1) * self.step) 41 | futures[self.executor.submit(*args)] = i 42 | concurrent.futures.wait(futures) 43 | 44 | def __call__(self, size=None): 45 | self.fill() 46 | return self.values.reshape(size) 47 | 48 | 49 | 50 | def __del__(self): 51 | self.executor.shutdown(False) 52 | 53 | 54 | 55 | rng = np.random.default_rng(12345) 56 | mrng = MultithreadedRNG(x.size, seed=12345) 57 | samplers = [np.random.normal, np.random.standard_normal, rng.standard_normal, mrng] 58 | 59 | for sampler in samplers: 60 | dyn = CBO(f, x=x, sampler=sampler) 61 | T = timeit.Timer(dyn.step) 62 | 63 | r = T.repeat(rep, number=50) 64 | best = sum(r)/rep 65 | print(str(sampler) + ' Mean ' + str(rep) + ': ' +str(best)[:6] + 's') -------------------------------------------------------------------------------- /cbx/utils/success.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def dist_to_min_success(x, x_true, tol=0.25, p=float('inf')): 4 | norm_diff = np.linalg.norm((x_true[None,...] - x.squeeze()).reshape(x.shape[0], -1), 5 | axis=-1, ord = p) 6 | idx = np.where(norm_diff < tol)[0] 7 | 8 | return {'num': len(idx), 9 | 'rate': len(idx)/x.shape[0], 10 | 'normdiff':norm_diff, 11 | 'idx':idx} 12 | 13 | class dist_to_min: 14 | def __init__(self, x_true, tol=0.25, p=float('inf')): 15 | self.x_true = x_true 16 | self.tol = tol 17 | self.p = p 18 | 19 | def __call__(self, dyn): 20 | return dist_to_min_success(dyn.best_particle, self.x_true, 21 | tol=self.tol, p=self.p) 22 | 23 | def value_success(x, f, thresh=0.1): 24 | vals = f(x) 25 | idx = np.where(vals < thresh)[0] 26 | 27 | return {'num': len(idx), 'rate': len(idx)/x.shape[0], 'idx':idx} 28 | 29 | class value_thresh: 30 | def __init__(self, thresh=0.1): 31 | self.thresh = thresh 32 | 33 | def __call__(self, dyn): 34 | return value_success(dyn.best_particle[:,None,...], dyn.f, thresh=self.thresh) 35 | 36 | class evaluation: 37 | def __init__(self, criteria=None, verbosity = 1): 38 | self.criteria = criteria 39 | self.verbosity = 1 40 | 41 | def __call__(self, dyn): 42 | idx = np.arange(dyn.M) 43 | for crit in self.criteria: 44 | result = crit(dyn) 45 | idx = np.intersect1d(result['idx'], idx) 46 | 47 | res = {'num': len(idx), 'rate': len(idx)/dyn.M, 'idx':idx} 48 | self.print_result(res) 49 | return res 50 | 51 | def print_result(self, res): 52 | if self.verbosity < 1: 53 | return 54 | print('------------------------------') 55 | print('Results of success evaluation:') 56 | print('Success Rate: ' + str(res['rate'])) 57 | print('Succesful runs: ' + str(res['num'])) 58 | print('Succesful idx: ' + str(res['idx'])) 59 | -------------------------------------------------------------------------------- /cbx/dynamics/cbs.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from .cbo import CBO 3 | from ..scheduler import scheduler 4 | #%% 5 | class CBS(CBO): 6 | def __init__(self, f, mode='sampling', noise='covariance', 7 | M=1, 8 | track_args=None, 9 | **kwargs): 10 | track_args = track_args if track_args is not None else{'names':[]} 11 | super().__init__(f, track_args=track_args, noise=noise, M=M, **kwargs) 12 | self.sigma = 1. 13 | 14 | if self.batched: 15 | raise NotImplementedError('Batched mode not implemented for CBS!') 16 | if self.x.ndim > 3: 17 | raise NotImplementedError('Multi dimensional domains not implemented for CBS! The particle should have the dimension M x N x d, where d is an integer!') 18 | 19 | if noise not in ['covariance', 'sampling']: 20 | raise warnings.warn('For CBS usually covariance or sampling noise is used!', stacklevel=2) 21 | 22 | self.noise_callable.mode = mode 23 | 24 | # def inner_step(self,): 25 | # self.consensus, energy = self.compute_consensus() 26 | # self.energy = energy 27 | # self.drift = self.x - self.consensus 28 | # self.x = self.consensus + self.exp_dt * self.drift + self.noise() 29 | 30 | def run(self, sched = 'default'): 31 | if self.verbosity > 0: 32 | print('.'*20) 33 | print('Starting Run with dynamic: ' + self.__class__.__name__) 34 | print('.'*20) 35 | 36 | if sched is None: 37 | sched = scheduler(self, []) 38 | elif sched == 'default': 39 | sched = self.default_sched() 40 | else: 41 | if not isinstance(sched, scheduler): 42 | raise RuntimeError('Unknonw scheduler specified!') 43 | 44 | while not self.terminate(): 45 | self.step() 46 | sched.update(self) 47 | 48 | def optimize(self, sched = 'default'): 49 | self.run(sched=sched) 50 | 51 | def default_sched(self,): 52 | return scheduler([]) 53 | 54 | def process_particles(self,): 55 | pass -------------------------------------------------------------------------------- /citation.cff: -------------------------------------------------------------------------------- 1 | cff-version: "1.2.0" 2 | authors: 3 | - family-names: Bailo 4 | given-names: Rafael 5 | orcid: "https://orcid.org/0000-0001-8018-3799" 6 | - family-names: Barbaro 7 | given-names: Alethea 8 | orcid: "https://orcid.org/0000-0001-9856-2818" 9 | - family-names: Gomes 10 | given-names: Susana N. 11 | orcid: "https://orcid.org/0000-0002-8731-367X" 12 | - family-names: Riedl 13 | given-names: Konstantin 14 | orcid: "https://orcid.org/0000-0002-2206-4334" 15 | - family-names: Roith 16 | given-names: Tim 17 | orcid: "https://orcid.org/0000-0001-8440-2928" 18 | - family-names: Totzeck 19 | given-names: Claudia 20 | orcid: "https://orcid.org/0000-0001-6283-7154" 21 | - family-names: Vaes 22 | given-names: Urbain 23 | orcid: "https://orcid.org/0000-0002-7629-7184" 24 | doi: 10.5281/zenodo.12207224 25 | message: If you use this software, please cite our article in the 26 | Journal of Open Source Software. 27 | preferred-citation: 28 | authors: 29 | - family-names: Bailo 30 | given-names: Rafael 31 | orcid: "https://orcid.org/0000-0001-8018-3799" 32 | - family-names: Barbaro 33 | given-names: Alethea 34 | orcid: "https://orcid.org/0000-0001-9856-2818" 35 | - family-names: Gomes 36 | given-names: Susana N. 37 | orcid: "https://orcid.org/0000-0002-8731-367X" 38 | - family-names: Riedl 39 | given-names: Konstantin 40 | orcid: "https://orcid.org/0000-0002-2206-4334" 41 | - family-names: Roith 42 | given-names: Tim 43 | orcid: "https://orcid.org/0000-0001-8440-2928" 44 | - family-names: Totzeck 45 | given-names: Claudia 46 | orcid: "https://orcid.org/0000-0001-6283-7154" 47 | - family-names: Vaes 48 | given-names: Urbain 49 | orcid: "https://orcid.org/0000-0002-7629-7184" 50 | date-published: 2024-06-21 51 | doi: 10.21105/joss.06611 52 | issn: 2475-9066 53 | issue: 98 54 | journal: Journal of Open Source Software 55 | publisher: 56 | name: Open Journals 57 | start: 6611 58 | title: "CBX: Python and Julia Packages for Consensus-Based Interacting 59 | Particle Methods" 60 | type: article 61 | url: "https://joss.theoj.org/papers/10.21105/joss.06611" 62 | volume: 9 63 | title: "CBX: Python and Julia Packages for Consensus-Based Interacting 64 | Particle Methods" 65 | -------------------------------------------------------------------------------- /cbx/dynamics/cbo.py: -------------------------------------------------------------------------------- 1 | from .pdyn import CBXDynamic 2 | 3 | def cbo_update(drift, lamda, dt, sigma, noise): 4 | return -lamda * dt * drift + sigma * noise 5 | #%% CBO 6 | class CBO(CBXDynamic): 7 | r"""Consensus-based optimization (CBO) class 8 | 9 | This class implements the CBO algorithm as described in [1]_. The algorithm 10 | is a particle dynamic algorithm that is used to minimize the objective function :math:`f(x)`. 11 | 12 | Parameters 13 | ---------- 14 | x : array_like, shape (N, d) 15 | The initial positions of the particles. For a system of :math:`N` particles, the i-th row of this array ``x[i,:]`` 16 | represents the position :math:`x_i` of the i-th particle. 17 | f : objective 18 | The objective function :math:`f(x)` of the system. 19 | dt : float, optional 20 | The parameter :math:`dt` of the system. The default is 0.1. 21 | lamda : float, optional 22 | The decay parameter :math:`\lambda` of the system. The default is 1.0. 23 | alpha : float, optional 24 | The heat parameter :math:`\alpha` of the system. The default is 1.0. 25 | noise : noise_model, optional 26 | The noise model that is used to compute the noise vector. The default is ``normal_noise(dt=0.1)``. 27 | sigma : float, optional 28 | The parameter :math:`\sigma` of the noise model. The default is 1.0. 29 | 30 | References 31 | ---------- 32 | .. [1] Pinnau, R., Totzeck, C., Tse, O., & Martin, S. (2017). A consensus-based model for global optimization and its mean-field limit. 33 | Mathematical Models and Methods in Applied Sciences, 27(01), 183-204. 34 | 35 | """ 36 | 37 | def __init__(self, f, **kwargs) -> None: 38 | super().__init__(f, **kwargs) 39 | 40 | def cbo_step(self,): 41 | # compute consensus, sets self.energy and self.consensus 42 | self.compute_consensus() 43 | # update drift and apply drift correction 44 | self.drift = self.correction(self.x[self.particle_idx] - self.consensus) 45 | # perform cbo update step 46 | self.x[self.particle_idx] += cbo_update( 47 | self.drift, self.lamda, self.dt, 48 | self.sigma, self.noise() 49 | ) 50 | inner_step = cbo_step 51 | 52 | 53 | -------------------------------------------------------------------------------- /cbx/dynamics/driftconstrainedcbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics import CBO 2 | import numpy as np 3 | from cbx.constraints import MultiConstraint, get_constraints 4 | #%% 5 | def solve_system(A, x): 6 | return np.linalg.solve(A, x[..., None])[..., 0] 7 | 8 | #%% 9 | def dc_inner_step(self,): 10 | self.compute_consensus() 11 | self.drift = self.x - self.consensus 12 | noise = self.sigma * self.noise() 13 | const_drift = (self.dt/ self.eps) * self.G.grad_squared_sum(self.x) 14 | scaled_drift = self.lamda * self.dt * self.drift 15 | 16 | x_tilde = scaled_drift + const_drift + noise 17 | # A = np.eye(self.d[0]) + (self.dt/ self.eps) * self.G.hessian_squared_sum(self.x) 18 | # self.x -= solve_system(A, x_tilde) 19 | self.x -= self.G.solve_Id_hessian_squared_sum( 20 | self.x, x_tilde, factor=(self.dt/ self.eps) 21 | ) 22 | 23 | 24 | #%% 25 | class DriftConstrainedCBO(CBO): 26 | ''' 27 | Implements the algorithm in [1] 28 | 29 | 30 | [1] Carrillo, José A., et al. 31 | "An interacting particle consensus method for constrained global 32 | optimization." arXiv preprint arXiv:2405.00891 (2024). 33 | ''' 34 | 35 | 36 | def __init__( 37 | self, f, 38 | constraints = None, eps=0.01, 39 | eps_indep=0.001, sigma_indep=0., 40 | **kwargs 41 | ): 42 | super().__init__(f, **kwargs) 43 | self.G = MultiConstraint(get_constraints(constraints)) 44 | self.eps = eps 45 | self.eps_indep = eps_indep 46 | self.sigma_indep = sigma_indep 47 | 48 | 49 | def inner_step(self,): 50 | self.indep_noise_step() 51 | dc_inner_step(self) 52 | 53 | def indep_noise_step(self,): 54 | if self.sigma_indep > 0 and (self.consensus is not None): 55 | while True: 56 | cxd = (np.linalg.norm(self.x - self.consensus, axis=-1)**2).mean(axis=-1)/self.d[0] 57 | idx = np.where(cxd < self.eps_indep) 58 | if len(idx[0]) == 0: 59 | break 60 | z = np.random.normal(0,1, size=((len(idx[0]),) + self.x.shape[1:])) 61 | self.x[idx[0], ...] += ( 62 | self.sigma_indep * 63 | self.dt**0.5 * 64 | z 65 | ) -------------------------------------------------------------------------------- /cbx/dynamics/adamcbo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Tuple 3 | #from scipy.special import logsumexp 4 | 5 | from .cbo import CBO 6 | 7 | class AdamCBO(CBO): 8 | """ 9 | AdamCBO is a variant of the CBO algorithm that uses Adam-like updates for the consensus-based optimization. 10 | This was introduced in the paper: 11 | "A consensus-based global optimization method with adaptive momentum estimation" by Jingrun Chen, Shi Jin, Liyao Lyu 12 | 13 | Parameters 14 | ---------- 15 | f : callable 16 | The objective function to be minimized. 17 | betas : Tuple[float, float], optional 18 | Coefficients for the first and second moment estimates, by default (0.9, 0.99). 19 | eps : float, optional 20 | A small constant for numerical stability, by default 1e-8. 21 | **kwargs : additional keyword arguments 22 | Additional parameters for the CBO algorithm, such as `lamda`, `sigma`, etc. 23 | """ 24 | 25 | def __init__( 26 | self, 27 | f, 28 | betas: Tuple[float, float] = (0.9, 0.99), 29 | eps: float = 1e-8, 30 | **kwargs 31 | ) -> None: 32 | super().__init__(f, **kwargs) 33 | self.betas = betas 34 | self.eps = eps 35 | self.m = np.zeros_like(self.x) 36 | self.v = np.zeros_like(self.x) 37 | 38 | def inner_step(self): 39 | # compute consensus, sets self.energy and self.consensus 40 | self.compute_consensus() 41 | # set drift 42 | self.drift = self.x[self.particle_idx] - self.consensus 43 | # update moments 44 | self.update_moments() 45 | 46 | # perform cbo update step 47 | self.x[self.particle_idx] -= ( 48 | self.lamda * 49 | ( 50 | (self.m[self.particle_idx] * self.m_factor) / 51 | ((self.v[self.particle_idx] * self.v_factor)**0.5 + self.eps) 52 | ) 53 | - self.sigma * self.noise() 54 | ) 55 | 56 | def update_moments(self): 57 | # update moments 58 | self.m[self.particle_idx] = self.betas[0] * self.m[self.particle_idx] + (1 - self.betas[0]) * self.drift 59 | self.v[self.particle_idx] = self.betas[1] * self.v[self.particle_idx] + (1 - self.betas[1]) * self.drift**2 60 | 61 | # update factors 62 | self.m_factor = 1/(1 - self.betas[0]**(self.it+1)) 63 | self.v_factor = 1/(1 - self.betas[1]**(self.it+1)) -------------------------------------------------------------------------------- /tests/plotting/test_plotting.py: -------------------------------------------------------------------------------- 1 | import cbx 2 | import pytest 3 | from cbx.plotting import PlotDynamic, PlotDynamicHistory 4 | import matplotlib 5 | matplotlib.use('Agg') 6 | 7 | class test_plot_dynamic: 8 | @pytest.fixture 9 | def f(self): 10 | return cbx.objectives.Quadratic() 11 | 12 | class Test_plot_dynamic(test_plot_dynamic): 13 | @pytest.fixture 14 | def plot(self): 15 | return PlotDynamic 16 | 17 | 18 | def test_plot_init(self, f, plot): 19 | class obj: 20 | def __init__(self, d): 21 | self.d = d 22 | def __call__(self, x): 23 | return sum(x[i]**2 for i in range(self.d)) 24 | g = obj(1) 25 | 26 | for d in range(1,5): 27 | g.d = d 28 | dyn = cbx.dynamics.CBO(g, d=d) 29 | dyn.step() 30 | plotter = plot(dyn) 31 | plotter.init_plot() 32 | 33 | @pytest.mark.filterwarnings('ignore::UserWarning') 34 | def test_plot_x(self, f, plot): 35 | dyn = cbx.dynamics.CBO(f, d=2) 36 | dyn.step() 37 | plotter = plot(dyn) 38 | plotter.init_plot() 39 | plotter.update() 40 | 41 | class Test_plot_dynamic_history(test_plot_dynamic): 42 | @pytest.fixture 43 | def plot(self): 44 | return PlotDynamicHistory 45 | 46 | def test_max_it(self, f, plot): 47 | dyn = cbx.dynamics.CBO(f, d=2, track_args={'names':['x']}, max_it=1) 48 | dyn.step() 49 | plotter = plot(dyn) 50 | assert plotter.max_it == 1 51 | 52 | def test_plot_at_ind_x(self, f, plot): 53 | dyn = cbx.dynamics.CBO(f, d=2, track_args={'names':['x']}) 54 | dyn.step() 55 | plotter = plot(dyn) 56 | plotter.plot_at_ind(0) 57 | 58 | def test_plot_at_ind_c(self, f, plot): 59 | dyn = cbx.dynamics.CBO(f, d=2, track_args={'names':['x', 'consensus']}) 60 | dyn.step() 61 | plotter = plot(dyn, plot_consensus=True) 62 | plotter.plot_at_ind(0) 63 | 64 | def test_plot_at_ind_d(self, f, plot): 65 | dyn = cbx.dynamics.CBO(f, d=2, track_args={'names':['x', 'drift']}) 66 | dyn.step() 67 | plotter = plot(dyn, plot_drift=True) 68 | plotter.plot_at_ind(0) 69 | 70 | @pytest.mark.filterwarnings('ignore::UserWarning') 71 | def test_run_plots(self, f, plot): 72 | dyn = cbx.dynamics.CBO(f, d=2, track_args={'names':['x']}, max_it=1) 73 | dyn.step() 74 | plotter = plot(dyn) 75 | plotter.run_plots() -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | CBXPy: Consensus Based Particle Dynamics in Python 3 | ================================================================ 4 | 5 | 6 | CBXPy is a python package implementing consensus based particle schemes. Originally designed for optimization problems 7 | 8 | .. math:: 9 | 10 | \min_{x \in \mathbb{R}^n} f(x), 11 | 12 | 13 | the scheme was introduced as CBO (Consensus Based Optimization) in [1]_. Given an ensemble of points :math:`x = (x_1, \ldots, x_N)`, the update reads 14 | 15 | .. math:: 16 | 17 | x^i \gets x^i - \lambda\, dt\, (x_i - c_\alpha(x)) + \sigma\, \sqrt{dt} |x^i - c_\alpha(x)| \xi^i 18 | 19 | where :math:`\xi_i` are i.i.d. standard normal random variables. The core element is the consensus point 20 | 21 | .. math:: 22 | 23 | c_\alpha(x) = \frac{\sum_{i=1}^N x^i\, \exp(-\alpha\, f(x^i))}{\sum_{i=1}^N \exp(-\alpha\, f(x^i))}. 24 | 25 | with a parameter :math:`\alpha>0`. The scheme can be extended to sampling problems known as CBS, clustering problems and opinion dynamics, which motivates the acronym 26 | **CBX**, indicating the flexibility of the scheme. 27 | 28 | Installation 29 | ------------ 30 | The package can be installed via ``pip`` 31 | 32 | .. code-block:: bash 33 | 34 | pip install cbx 35 | 36 | Simple Usage Example 37 | -------------------- 38 | 39 | The following example shows how to minimize a function using CBXPy 40 | 41 | :: 42 | 43 | from cbx.dynamics import CBO 44 | 45 | f = lambda x: x[0]**2 + x[1]**2 46 | dyn = CBO(f, d=2) 47 | x = dyn.optimize() 48 | 49 | More Examples 50 | ------------- 51 | 52 | .. nblinkgallery:: 53 | 54 | /examples/nns/mnist.ipynb 55 | /examples/simple_example.ipynb 56 | /examples/custom_noise.ipynb 57 | /examples/polarcbo.ipynb 58 | /examples/onedim_example.ipynb 59 | /examples/low_level.ipynb 60 | /examples/sampling.ipynb 61 | /examples/MirrorElasticNet.ipynb 62 | 63 | 64 | 65 | Documentation 66 | ------------- 67 | 68 | The functionality of the package is documented in the user guide. For a specifics about the implementation, we refer to the API. 69 | 70 | .. toctree:: 71 | :maxdepth: 1 72 | 73 | userguide/index 74 | api/index 75 | 76 | 77 | References 78 | ---------- 79 | 80 | .. [1] Pinnau, R., Totzeck, C., Tse, O., & Martin, S. (2017). A consensus-based model for global optimization and its mean-field limit. Mathematical Models and Methods in Applied Sciences, 27(01), 183-204. 81 | 82 | Indices and tables 83 | ================== 84 | 85 | * :ref:`genindex` 86 | * :ref:`modindex` 87 | * :ref:`search` 88 | -------------------------------------------------------------------------------- /docs/userguide/plotting.rst: -------------------------------------------------------------------------------- 1 | Plotting 2 | ======== 3 | 4 | The module :mod:`cbx.plotting` provides some plotting functionality, that help to visualize the dynamic. One has the option to directly plot each iteration as the dynamic runs, or alternatively, save the results in the history of the dynamic and visualize it after it terminated. 5 | 6 | Visualizing during the run 7 | -------------------------- 8 | 9 | If you want to visualize the dynamic while it runs, you can use the class :class:`plot_dynamic `: 10 | 11 | >>> from cbx.dynamics import CBXDynamic 12 | >>> from cbx.plotting import plot_dynamic 13 | >>> dyn = CBXDynamic(lambda x:x**2, d=1) 14 | >>> plotter = plot_dynamic(dyn) 15 | >>> plotter.init_plot() 16 | 17 | Here, it is important to call the method :meth:`init_plot `, which initializes the necessary parameters for the plotting. After each step of the dynamic, the method :func:`update ` updates the plot, according to changes in the dynamic. The following objects can be plotted: 18 | 19 | * the ensemble: a '1D' or '2D' scatter plot of the ensemble. If the array has more than two dimensions, the dimensions specified in ``dims`` are used. In order to be consistent with the drift and the consensus, we always use ``dyn.x_old`` as the ensemble. 20 | * the consensus: a '1D' or '2D' scatter plot of the consensus point. If the array has more than two dimensions, the dimensions specified in ``dims`` are used. You can enable plotting of the consensus by setting ``plot_consensus=True``. 21 | * the drift: a quiver plot of the drift. This only works if the dimension is at least two. If the array has more than two dimensions, the dimensions specified in ``dims`` are used. You can enable plotting of the drift by setting ``plot_drift=True``. 22 | 23 | Visualizing after the run 24 | ------------------------ 25 | 26 | You can also visualize the content of the history, by using the class :class:`plot_dynamic_history `. This only works, if you specified to track the ensemble. 27 | 28 | >>> from cbx.dynamics import CBXDynamic 29 | >>> dyn = CBXDynamic(lambda x:x**2, d=1, track=['x'], max_it = 10) 30 | >>> dyn.optimize() 31 | >>> plotter = plot_dynamic(dyn) 32 | >>> plotter.run_plots() 33 | 34 | The class :class:`plot_dynamic_history ` uses the function :meth:`plot_at_ind ` to plot the content of the history at a certain index. This function can also be used for sliders. 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # vscode 7 | .vscode/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # pngs 105 | *.png 106 | !docs/_static/cbx-logo.png 107 | !docs/_static/cbx-logo.ico 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | -------------------------------------------------------------------------------- /cbx/correction.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable 2 | from numpy.typing import ArrayLike 3 | import numpy as np 4 | 5 | def get_correction(name, **kwargs): 6 | if name == 'no_correction': 7 | return no_correction() 8 | elif name == 'heavi_side': 9 | return heavi_side_correction() 10 | elif name == 'heavi_side_reg': 11 | eps = kwargs.get('eps', 1e-3) 12 | return heavi_side_reg_correction(eps=eps) 13 | else: 14 | raise ValueError('Unknown correction ' + str(name)) 15 | 16 | class correction: 17 | def __init__(self): 18 | pass 19 | 20 | class no_correction(correction): 21 | """ 22 | The class if no correction is specified. Is equal to the identity. 23 | 24 | Parameters: 25 | x: The input value. 26 | 27 | Returns: 28 | The input value without any correction. 29 | """ 30 | 31 | def __call__(self, dyn, x): 32 | return self.correct(x) 33 | 34 | def correct(self, x:Any) -> Any: 35 | """ 36 | Return the identity 37 | 38 | Parameters 39 | ---------- 40 | x : Any 41 | The input array. 42 | 43 | Returns 44 | ------- 45 | x: Any 46 | The input array. 47 | """ 48 | 49 | return x 50 | 51 | class heavi_side_correction(correction): 52 | """ 53 | Calculate the Heaviside correction for the given input. 54 | 55 | """ 56 | 57 | def __call__(self, dyn, x : ArrayLike) -> ArrayLike: 58 | """ 59 | Calculate the Heaviside correction for the given input. 60 | 61 | Parameters 62 | ---------- 63 | dyn : CBXDynamic 64 | The CBXDynamic object. 65 | x : ndarray 66 | The input array. 67 | 68 | Returns 69 | ------- 70 | ndarray 71 | The Heaviside correction value. 72 | 73 | .. note:: 74 | This function evaluates the objective function on the consensus, therfore the number of function evaluations is increased by the consensus size. 75 | """ 76 | dyn.num_f_eval += dyn.consensus.shape[0] # update number of function evaluations 77 | return self.correct(x, dyn.f, dyn.energy, dyn.consensus) 78 | 79 | def correct(self, x:ArrayLike, f: Callable, energy: ArrayLike, consensus: ArrayLike) -> ArrayLike: 80 | z = energy - f(consensus) 81 | return x * np.where(z > 0, 1,0)[...,None] 82 | 83 | class heavi_side_reg_correction(heavi_side_correction): 84 | """ 85 | Calculate the Heaviside regularized correction. 86 | """ 87 | 88 | def __init__(self, eps=1e-3): 89 | super().__init__() 90 | self.eps = eps 91 | 92 | def correct(self, x:ArrayLike, f: Callable, energy: ArrayLike, consensus: ArrayLike) -> ArrayLike: 93 | z = energy - f(consensus) 94 | return x * (0.5 + 0.5 * np.tanh(z/self.eps))[...,None] 95 | 96 | 97 | -------------------------------------------------------------------------------- /tests/dynamics/test_polarcbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics import PolarCBO 2 | import pytest 3 | from test_abstraction import test_abstract_dynamic 4 | import numpy as np 5 | 6 | class Test_polarcbo(test_abstract_dynamic): 7 | 8 | @pytest.fixture 9 | def dynamic(self): 10 | return PolarCBO 11 | 12 | def test_step_eval(self, f, dynamic): 13 | dyn = dynamic(f, d=5, M=7, N=5) 14 | dyn.step() 15 | assert dyn.it == 1 16 | 17 | def test_Gaussian_kernel(self, f, dynamic): 18 | dyn = dynamic(f, d=3, M=7, N=5, kernel='Gaussian') 19 | dyn.step() 20 | eval = dyn.kernel(dyn.x[:,None,...], dyn.x[:,:,None,...]) 21 | assert dyn.consensus.shape == dyn.x.shape 22 | assert eval.shape == (7,5,5) 23 | 24 | def test_Laplace_kernel(self, f, dynamic): 25 | dyn = dynamic(f, d=3, M=7, N=5, kernel='Laplace') 26 | dyn.step() 27 | eval = dyn.kernel(dyn.x[:,None,...], dyn.x[:,:,None,...]) 28 | assert dyn.consensus.shape == dyn.x.shape 29 | assert eval.shape == (7,5,5) 30 | 31 | def test_Constant_kernel(self, f, dynamic): 32 | dyn = dynamic(f, d=3, M=7, N=5, kernel='Constant') 33 | eval = dyn.kernel(dyn.x[:,None,...], dyn.x[:,:,None,...]) 34 | dyn.step() 35 | assert dyn.consensus.shape == dyn.x.shape 36 | assert eval.shape == (7,5,5) 37 | 38 | def test_IQ_kernel(self, f, dynamic): 39 | dyn = dynamic(f, d=3, M=7, N=5, kernel='InverseQuadratic') 40 | eval = dyn.kernel(dyn.x[:,None,...], dyn.x[:,:,None,...]) 41 | dyn.step() 42 | assert dyn.consensus.shape == dyn.x.shape 43 | assert eval.shape == (7,5,5) 44 | 45 | def test_Taz_kernel(self, f, dynamic): 46 | dyn = dynamic(f, d=3, M=7, N=5, kernel='Taz') 47 | dyn.step() 48 | assert dyn.consensus.shape == dyn.x.shape 49 | 50 | def test_consensus_shape(self, f, dynamic): 51 | dyn = dynamic(f, d=3, M=7, N=5) 52 | dyn.step() 53 | assert dyn.consensus.shape == (7, 5, 3) 54 | 55 | def test_consensus_value(self, f, dynamic): 56 | dyn = dynamic(f, d=3, M=7, N=5, kernel_factor_mode='const') 57 | c = np.zeros(dyn.x.shape) 58 | dd = dyn.kernel(dyn.x[:,None,...], dyn.x[:,:,None,...]) 59 | d = np.zeros((7,5,5)) 60 | for m in range(dyn.M): 61 | for n in range(dyn.N): 62 | nom = np.zeros(dyn.d) 63 | denom = 0. 64 | for nn in range(dyn.N): 65 | d[m, n, nn] = np.exp(-1/(2*dyn.kernel.kappa**2) * ((dyn.x[m,n,...] - dyn.x[m,nn,...])**2).sum()) 66 | w = d[m, n, nn] * np.exp(-dyn.alpha[m] * dyn.f(dyn.x[m,nn,...]))[0,0] 67 | nom += w * dyn.x[m,nn,:] 68 | denom += w 69 | c[m,n,:] = nom / denom 70 | dyn.compute_consensus() 71 | assert np.allclose(dd, d) 72 | assert np.allclose(dyn.consensus, c) 73 | 74 | 75 | -------------------------------------------------------------------------------- /cbx/utils/objective_handling.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | #from typing import Callable, Any 3 | 4 | def _promote_objective(f, f_dim): 5 | if not callable(f): 6 | raise TypeError("Objective function must be callable.") 7 | if f_dim == '3D': 8 | return f 9 | elif f_dim == '2D': 10 | return cbx_objective_f2D(f) 11 | elif f_dim == '1D': 12 | return cbx_objective_f1D(f) 13 | else: 14 | raise ValueError("f_dim must be '1D', '2D' or '3D'.") 15 | 16 | 17 | class cbx_objective: 18 | def __init__(self, f_extra=None): 19 | super().__init__() 20 | self.num_eval = 0 21 | 22 | def __call__(self, x): 23 | """ 24 | Applies the objective function to the input x and counts th number of evaluations. 25 | 26 | Parameters 27 | ---------- 28 | x 29 | The input to the objective function. 30 | 31 | Returns 32 | ------- 33 | The output of the objective function. 34 | """ 35 | 36 | self.num_eval += np.prod(np.atleast_2d(x).shape[:-1], dtype = int) 37 | return self.apply(x) 38 | 39 | def apply(self, x): 40 | NotImplementedError(f"Objective [{type(self).__name__}] is missing the required \"apply\" function") 41 | 42 | def reset(self,): 43 | self.num_eval = 0 44 | 45 | 46 | class cbx_objective_fh(cbx_objective): 47 | """ 48 | Creates a cbx_objective from a function handle. 49 | """ 50 | 51 | def __init__(self, f): 52 | super().__init__() 53 | self.f = f 54 | 55 | def apply(self, x): 56 | """ 57 | Applies the function f to the input x. 58 | 59 | Parameters 60 | ---------- 61 | x 62 | The input to the function. 63 | 64 | Returns 65 | ------- 66 | The output of the function. 67 | """ 68 | 69 | return self.f(x) 70 | 71 | class cbx_objective_f1D(cbx_objective_fh): 72 | def __init__(self, f): 73 | super().__init__(f) 74 | 75 | def apply(self, x): 76 | x = np.atleast_2d(x) 77 | return np.apply_along_axis(self.f, 1, x.reshape(-1, x.shape[-1])).reshape(-1,x.shape[-2]) 78 | 79 | 80 | class cbx_objective_f2D(cbx_objective_fh): 81 | """ 82 | A class for handling 2D objective functions. 83 | """ 84 | def __init__(self, f): 85 | super().__init__(f) 86 | 87 | def apply(self, x): 88 | """ 89 | Applies the function f to the input x. 90 | 91 | Parameters 92 | ---------- 93 | x 94 | The input to the function. 95 | 96 | Returns 97 | ------- 98 | The output of the function. 99 | """ 100 | x = np.atleast_2d(x) 101 | return self.f(np.atleast_2d(x.reshape(-1, x.shape[-1]))).reshape(-1,x.shape[-2]) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CBXPy 2 | 3 | Hey, great that you found your way here and want to contribute to CBXPy. And thanks a lot that you are going through these guidelines! We are always happy for your suggestions or improvements, e.g.: 4 | 5 | * bug reports, 6 | * bug fixes, 7 | * feature developments, 8 | * documentation. 9 | 10 | ## How to contribute 11 | 12 | The best way to contribute to the repository is to create a fork. After committing your desired changes, you can create a pull request. Please make sure, that the tests are passing before submitting. The unit tests employ ``pytest`` and are implemented as a [GitHub workflow](https://github.com/PdIPS/CBXpy/blob/main/.github/workflows/Tests.yml). Therefore, the tests can be run as a [GitHub Action](https://docs.github.com/de/actions). 13 | 14 | ## Reporting a Bug 15 | 16 | Although, the nature of CBXPy does not directly indicate possible security issues: If you find a security vulnerability, do **NOT** open an issue. Email tim.roith@desy.de instead. 17 | 18 | Other than that, please open an issue [here](https://github.com/PdIPS/CBXpy/issues). The bug report template gives more details on how to open an issue for a bug report. 19 | 20 | ## Adding a feature 21 | 22 | Since CBX aims to capture the growing field of consensus-based particle methods in one package, additions and implementations of new algorithm variants are very welcome. A good starting point to understand the mechanisms of CBXPy is the [documentation](https://pdips.github.io/CBXpy/). The most important aspects of the CBXPy implementation is explained there. If anything is still unclear, you can take a look at the [discussion forum](https://github.com/orgs/PdIPS/discussions). 23 | 24 | Apart from that, we list the following aspects of adding new features and algorithms: 25 | 26 | * **Is the feature novel?** For example, if your proposed algorithm is a special case of an existing implementation, it is preferred to employ the existing function with special parameters, instead of writing new code. 27 | 28 | * **Are multirun-ensembles supported?** As explained in the documentation [here](https://pdips.github.io/CBXpy/userguide/dynamics.html) ensemble arrays are always of the shape $(M\times N\times d_1, \ldots, d_s)$, where $M$ denotes the number of runs and $N$ denotes the number of particles. Your feature must be able to deal with this ensemble structure. 29 | 30 | * **Does your code avoid for-loops**? While it is very intuitive, to write operations over runs and particles as for-loops, this usually leads to performance bottlenecks due to the for-loop in Python. CBXPy does **not** aim for ultimate high-performance, but reasonable optimization with the tools provided by ``numpy`` is expected. In practice, this means, we always try to avoid for-loops by using array operations in ``numpy``. 31 | 32 | ## Code Review 33 | 34 | Code reviews are currently only conducted by [TimRoith](https://github.com/TimRoith). A first reply on a new pull request can be expected within a week. You can also write a mail to tim.roith@desy.de if you feel that your request got lost. 35 | -------------------------------------------------------------------------------- /docs/examples/JOSS/JOSS_Example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cbx as cbx 3 | import cbx.objectives as obj 4 | from cbx.scheduler import multiply 5 | from cbx.plotting import contour_2D 6 | import matplotlib.pyplot as plt 7 | from matplotlib.lines import Line2D 8 | from scipy.interpolate import CubicSpline 9 | from matplotlib.colors import ListedColormap 10 | #%% 11 | kwargs = {'alpha': 1.1, 12 | 'dt': 0.001, 13 | 'sigma': 0.1, 14 | 'lamda': 1.0, 15 | 'd': 2, 16 | 'track_args': {'save_int':1000, 'names':['x']}, 17 | 'max_it': 5000, 18 | 'N': 5, 19 | 'M': 1,} 20 | 21 | #%% Set seed and define the objective function 22 | np.random.seed(42) 23 | f = obj.Ackley() 24 | 25 | #%% Define the initial positions of the particles 26 | x = np.loadtxt('initial.txt',)[None, :,:] 27 | dyn = cbx.dynamics.CBO(f, x=x, **kwargs) 28 | sched = multiply(name='alpha', factor=1.01, maximum=1e3) 29 | x_best = dyn.optimize(sched = sched) 30 | 31 | #%% plot particle history 32 | x_hist = np.array(dyn.history['x'])[1:,...] 33 | #idx = [i for i in range(x_hist.shape[0]) if ((i%5==0) or i<4)] 34 | #x_hist = x_hist[idx, ...] 35 | x_min = -4 36 | x_max = 4 37 | 38 | plt.close('all') 39 | plt.rcParams.update({ 40 | #"text.usetex": True, 41 | "font.family": 'STIXGeneral', 42 | 'mathtext.fontset':'stix', 43 | 'font.size':13 44 | }) 45 | 46 | fig, ax = plt.subplots(1,1) 47 | cf = contour_2D(f, ax=ax, num_pts=1000, 48 | x_min=-4, x_max =4., cmap='binary', 49 | levels=50) 50 | cbar = plt.colorbar(cf) 51 | cbar.set_label('Objective value', rotation=270, labelpad=10) 52 | t = np.linspace(0,1, x_hist.shape[0]) 53 | t_eval = np.linspace(0,1, 100 * x_hist.shape[0]) 54 | colors = ['xkcd:spruce', 55 | 'xkcd:seaweed','xkcd:minty green', 56 | 'xkcd:light seafoam', 'xkcd:grapefruit', 57 | 'xkcd:grapefruit','xkcd:grapefruit', 'xkcd:rose pink'] 58 | color_map = ListedColormap(colors) 59 | ax.axis('off') 60 | ax.set_aspect('equal') 61 | ax.set_xlim(x_min,x_max) 62 | ax.set_ylim(x_min,x_max) 63 | 64 | 65 | for i in range(x_hist.shape[-2]): 66 | cs_0 = CubicSpline(t, x_hist[:, 0, i, 0]) 67 | cs_1 = CubicSpline(t, x_hist[:, 0, i, 1]) 68 | 69 | line = Line2D(cs_0(t_eval), cs_1(t_eval), 70 | color='xkcd:spruce',#xkcd:strong blue', 71 | alpha=.5,linewidth=3, 72 | linestyle='dotted', 73 | dash_capstyle='round') 74 | ax.add_line(line) 75 | sc_idx = 4 76 | sc = ax.scatter(x_hist[:sc_idx, 0, i, 0], x_hist[:sc_idx, 0, i, 1], 77 | #color=colors[:sc_idx], 78 | cmap = color_map, 79 | c = [i for i in range(sc_idx)], 80 | s=52,zorder=3) 81 | # plt.legend(handles=sc.legend_elements()[0], 82 | # labels=['t = ' + str(i * kwargs['track_args']['save_int']*dyn.dt) 83 | # for i in range(sc_idx)], 84 | # loc='lower right', 85 | # title='Particle evolution') 86 | #%% 87 | save = True 88 | if save: 89 | plt.tight_layout(pad=0.0, h_pad=0, w_pad=0) 90 | plt.savefig('JOSS.pdf') 91 | -------------------------------------------------------------------------------- /tests/objectives/test_objectives.py: -------------------------------------------------------------------------------- 1 | import cbx.objectives as cob 2 | import pytest 3 | import numpy as np 4 | 5 | class test_abstract_objective: 6 | @pytest.fixture 7 | def f(self): 8 | raise NotImplementedError() 9 | 10 | def test_eval(self, f): 11 | x = np.random.uniform(-1,1,(6,5,7)) 12 | z = f(x) 13 | assert z.shape == (6,5) 14 | 15 | class Test_Rastrigin(test_abstract_objective): 16 | @pytest.fixture 17 | def f(self): 18 | return cob.Rastrigin() 19 | 20 | class Test_Rastrigin_multimodal(test_abstract_objective): 21 | @pytest.fixture 22 | def f(self): 23 | return cob.Rastrigin_multimodal() 24 | 25 | class Test_Ackley(test_abstract_objective): 26 | @pytest.fixture 27 | def f(self): 28 | return cob.Ackley() 29 | 30 | class Test_Ackley_multimodal(test_abstract_objective): 31 | @pytest.fixture 32 | def f(self): 33 | return cob.Ackley_multimodal() 34 | 35 | class Test_three_hump_camel(test_abstract_objective): 36 | @pytest.fixture 37 | def f(self): 38 | return cob.three_hump_camel() 39 | 40 | class Test_McCormick(test_abstract_objective): 41 | @pytest.fixture 42 | def f(self): 43 | return cob.McCormick() 44 | 45 | class Test_Rosenbrock(test_abstract_objective): 46 | @pytest.fixture 47 | def f(self): 48 | return cob.Rosenbrock() 49 | 50 | class Test_accelerated_sinus(test_abstract_objective): 51 | @pytest.fixture 52 | def f(self): 53 | return cob.accelerated_sinus() 54 | 55 | class Test_nd_sinus(test_abstract_objective): 56 | @pytest.fixture 57 | def f(self): 58 | return cob.nd_sinus() 59 | 60 | class Test_p_4th_order(test_abstract_objective): 61 | @pytest.fixture 62 | def f(self): 63 | return cob.p_4th_order() 64 | 65 | class Test_Quadratic(test_abstract_objective): 66 | @pytest.fixture 67 | def f(self): 68 | return cob.Quadratic() 69 | 70 | class Test_Banana(test_abstract_objective): 71 | @pytest.fixture 72 | def f(self): 73 | return cob.Banana() 74 | 75 | class Test_Bimodal(test_abstract_objective): 76 | @pytest.fixture 77 | def f(self): 78 | return cob.Bimodal() 79 | 80 | class Test_Unimodal(test_abstract_objective): 81 | @pytest.fixture 82 | def f(self): 83 | return cob.Unimodal() 84 | 85 | class Test_Bukin6(test_abstract_objective): 86 | @pytest.fixture 87 | def f(self): 88 | return cob.Bukin6() 89 | 90 | class Test_cross_in_tray(test_abstract_objective): 91 | @pytest.fixture 92 | def f(self): 93 | return cob.cross_in_tray() 94 | 95 | class Test_Easom(test_abstract_objective): 96 | @pytest.fixture 97 | def f(self): 98 | return cob.Easom() 99 | 100 | class Test_drop_wave(test_abstract_objective): 101 | @pytest.fixture 102 | def f(self): 103 | return cob.drop_wave() 104 | 105 | class Test_Holder_table(test_abstract_objective): 106 | @pytest.fixture 107 | def f(self): 108 | return cob.Holder_table() 109 | 110 | class Test_snowflake(test_abstract_objective): 111 | @pytest.fixture 112 | def f(self): 113 | return cob.snowflake() -------------------------------------------------------------------------------- /docs/examples/custom_noise.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "c70df536-cea0-4cdf-bd6a-601824a52768", 6 | "metadata": {}, 7 | "source": [ 8 | "# Custom Noise CBX\n", 9 | "\n", 10 | "This notebook showcases, how to use a custom noise function." 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "id": "33394a7d-caa6-4f01-aa88-4dd69d5c400d", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from cbx.dynamics import CBO\n", 21 | "import numpy as np" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "id": "ef05a88a-d000-4ea8-af21-714df66858fa", 27 | "metadata": {}, 28 | "source": [ 29 | "## Define the custom noise function\n", 30 | "\n", 31 | "In this case we select that the noise should be zero" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 2, 37 | "id": "319270d9-01d4-49c0-8ac5-ab94bd6c327b", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "def custom_noise(dyn):\n", 42 | " return np.zeros(dyn.drift.shape)" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "id": "08dac699-4772-4409-8e25-2a398216a2f4", 48 | "metadata": {}, 49 | "source": [ 50 | "## Define a loss function and test the method" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 3, 56 | "id": "aae8cac4-7a04-41a2-b9ee-a0c8c26e27da", 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "def f(x):\n", 61 | " return np.linalg.norm(x, axis=-1)\n", 62 | "\n", 63 | "x0 = np.random.normal(0,1,(4,7,26))\n", 64 | "dyn_cn = CBO(f, x=x0, noise=custom_noise, max_it=10, verbosity = 0)\n", 65 | "x_cn = dyn_cn.optimize()" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "id": "762bcbf2-6861-4c21-8475-3e6b45e35fdb", 71 | "metadata": {}, 72 | "source": [ 73 | "Using this noise is equivalent to specifying ``sigma=0`` with standard CBO. So let's test, if this is the case:" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 4, 79 | "id": "f1f11124-6a10-4e54-abef-4168cbfd6dbc", 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "name": "stdout", 84 | "output_type": "stream", 85 | "text": [ 86 | "L-infinty Error between the custom noise solution and standard CBO with sigma=0: 0.0\n" 87 | ] 88 | } 89 | ], 90 | "source": [ 91 | "dyn = CBO(f, x=x0, sigma=0, max_it=10, verbosity = 0)\n", 92 | "x = dyn.optimize()\n", 93 | "print('L-infinty Error between the custom noise solution and standard CBO with sigma=0: ' + str(np.abs(x-x_cn).max()))" 94 | ] 95 | } 96 | ], 97 | "metadata": { 98 | "kernelspec": { 99 | "display_name": "Python 3", 100 | "language": "python", 101 | "name": "python3" 102 | }, 103 | "language_info": { 104 | "codemirror_mode": { 105 | "name": "ipython", 106 | "version": 3 107 | }, 108 | "file_extension": ".py", 109 | "mimetype": "text/x-python", 110 | "name": "python", 111 | "nbconvert_exporter": "python", 112 | "pygments_lexer": "ipython3", 113 | "version": "3.12.4" 114 | } 115 | }, 116 | "nbformat": 4, 117 | "nbformat_minor": 5 118 | } 119 | -------------------------------------------------------------------------------- /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 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'CBX' 21 | copyright = '2024, Tim Roith' 22 | author = 'Tim Roith' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = 'v0.1.6' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'nbsphinx', 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.viewcode', 37 | 'sphinx.ext.napoleon', 38 | 'sphinx.ext.mathjax', 39 | 'sphinx_design', 40 | 'matplotlib.sphinxext.plot_directive', 41 | 'sphinx.ext.autosummary' 42 | ] 43 | autosummary_generate = True 44 | #numpydoc_show_class_members = False 45 | numpydoc_show_class_members = False 46 | autodoc_member_order = 'bysource' 47 | autodoc_typehints = "none" 48 | #autoapi_dirs = ['../polarcbo'] 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | #autoapi_generate_api_docs = False 52 | 53 | # List of patterns, relative to source directory, that match files and 54 | # directories to ignore when looking for source files. 55 | # This pattern also affects html_static_path and html_extra_path. 56 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 57 | 58 | nbsphinx_execute = 'never' 59 | 60 | 61 | # -- Options for HTML output ------------------------------------------------- 62 | 63 | # The theme to use for HTML and HTML Help pages. See the documentation for 64 | # a list of builtin themes. 65 | # 66 | #html_theme = 'sphinx_rtd_theme' 67 | html_theme = "pydata_sphinx_theme" 68 | html_favicon = '_static/cbx-logo.ico' 69 | #html_theme = 'alabaster' 70 | 71 | # Add any paths that contain custom static files (such as style sheets) here, 72 | # relative to this directory. They are copied after the builtin static files, 73 | # so a file named "default.css" will overwrite the builtin "default.css". 74 | html_static_path = ['_static'] 75 | html_logo = "_static/cbx-logo.png" 76 | html_theme_options = { 77 | "icon_links": [ 78 | { 79 | "name": "GitHub", 80 | "url": "https://github.com/PdIPS/CBXpy", 81 | "icon": "fab fa-github-square", 82 | "type": "fontawesome", 83 | }, 84 | ], 85 | "favicons": [ 86 | { 87 | "rel": "icon", 88 | "sizes": "16x16", 89 | "href": "cbx_py32x32.ico", 90 | }, 91 | { 92 | "rel": "icon", 93 | "sizes": "32x32", 94 | "href": "cbx_py32x32.ico", 95 | }, 96 | ] 97 | } 98 | 99 | nbsphinx_thumbnails = { 100 | 'examples/nns/mnist': '_static/cbx-logo.png', 101 | 'examples/simple_example': '_static/cbx-logo.png', 102 | 'examples/custom_noise': '_static/cbx-logo.png', 103 | 'examples/low_level': '_static/cbx-logo.png', 104 | 'examples/sampling': '_static/cbx-logo.png', 105 | 'examples/success_evaluation': '_static/cbx-logo.png', 106 | } 107 | 108 | def setup(app): 109 | app.add_css_file('css/style.css') -------------------------------------------------------------------------------- /docs/_static/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #faab00; 3 | --sd-color-primary: #faab00; 4 | --sd-color-secondary: #6c757d; 5 | --sd-color-success: #28a745; 6 | --sd-color-info: #faab00; 7 | --sd-color-warning: #f0b37e; 8 | --sd-color-danger: #dc3545; 9 | --sd-color-light: #f8f9fa; 10 | --sd-color-muted: #6c757d; 11 | --sd-color-dark: #212529; 12 | --sd-color-primary-highlight: #0069d9; 13 | --sd-color-secondary-highlight: #5c636a; 14 | --sd-color-success-highlight: #228e3b; 15 | --sd-color-info-highlight: #148a9c; 16 | --sd-color-warning-highlight: #cc986b; 17 | --sd-color-danger-highlight: #bb2d3b; 18 | --sd-color-light-highlight: #d3d4d5; 19 | --sd-color-muted-highlight: #5c636a; 20 | --sd-color-dark-highlight: #1c1f23; 21 | --sd-color-primary-text: #fff; 22 | --sd-color-secondary-text: #fff; 23 | --sd-color-success-text: #fff; 24 | --sd-color-info-text: #fff; 25 | --sd-color-warning-text: #212529; 26 | --sd-color-danger-text: #fff; 27 | --sd-color-light-text: #212529; 28 | --sd-color-muted-text: #fff; 29 | --sd-color-dark-text: #fff; 30 | --sd-color-shadow: rgba(0, 0, 0, 0.15); 31 | --sd-color-card-border: rgba(0, 0, 0, 0.125); 32 | --sd-color-card-border-hover: hsla(231, 99%, 66%, 1); 33 | --sd-color-card-background: transparent; 34 | --sd-color-card-text: inherit; 35 | --sd-color-card-header: transparent; 36 | --sd-color-card-footer: transparent; 37 | --sd-color-tabs-label-active: hsla(231, 99%, 66%, 1); 38 | --sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1); 39 | --sd-color-tabs-label-inactive: hsl(0, 0%, 66%); 40 | --sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1); 41 | --sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62); 42 | --sd-color-tabs-underline-inactive: transparent; 43 | --sd-color-tabs-overline: rgb(222, 222, 222); 44 | --sd-color-tabs-underline: rgb(222, 222, 222); 45 | --sd-fontsize-tabs-label: 1rem 46 | } 47 | 48 | .wy-side-nav-search { 49 | background-color: #ffffff; 50 | } 51 | 52 | .wy-nav-side { 53 | background: #faab0048; 54 | color: #000000; 55 | } 56 | 57 | .wy-menu-vertical a { 58 | line-height: 18px; 59 | padding: 0.4045em 1.618em; 60 | display: block; 61 | position: relative; 62 | font-size: 90%; 63 | color: #000000; 64 | } 65 | 66 | .wy-table-responsive table { 67 | border: none; 68 | border-color: #ffffff !important; 69 | /* table-layout: fixed; */ 70 | } 71 | 72 | .wy-table-responsive table tbody td .pre { 73 | background: #ffffff; 74 | color: #000000; 75 | font-size: 87.5%; 76 | } 77 | 78 | .wy-table-responsive table tbody .row-odd { 79 | background-color: #faab0048; 80 | } 81 | 82 | .wy-table-responsive table thead tr { 83 | border-bottom: 2px solid #ffffff; 84 | } 85 | 86 | .wy-table-responsive table thead th { 87 | line-height: 1.75rem; 88 | padding-left: 0.9375rem; 89 | padding-right: 0.9375rem; 90 | } 91 | 92 | .wy-table-responsive table tbody td { 93 | color: #000000; 94 | white-space: normal; 95 | padding: 0.9375rem; 96 | font-size: 1rem; 97 | line-height: 1.375rem; 98 | } 99 | 100 | .wy-table-responsive table tbody td .pre { 101 | background: #ffffff; 102 | color: #000000; 103 | font-size: 100%; 104 | } 105 | .wy-table-responsive table tbody td code { 106 | font-size: 100%; 107 | } 108 | 109 | 110 | code.literal { 111 | color: #000000 !important; 112 | background-color: #fbfbfb !important; 113 | } 114 | 115 | /*.wy-nav-content { 116 | max-width: none; 117 | } 118 | 119 | /* override table width restrictions */ 120 | .wy-table-responsive table td, .wy-table-responsive table th { 121 | /* !important prevents the common CSS stylesheets from 122 | overriding this as on RTD they are loaded after this stylesheet */ 123 | white-space: normal !important; 124 | } 125 | 126 | 127 | .wy-table-responsive { 128 | overflow: visible !important; 129 | } -------------------------------------------------------------------------------- /cbx/regularizers.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from cbx.scheduler import param_update 3 | 4 | #%% 5 | class L0: 6 | def __init__(self, lamda=1.): 7 | self.lamda = lamda 8 | 9 | def __call__(self, x): 10 | return self.lamda * (~np.isclose(x, 0)).sum(axis=-1) 11 | 12 | class L1: 13 | def __init__(self, lamda=1.): 14 | self.lamda = lamda 15 | 16 | def __call__(self, x): 17 | return self.lamda * np.linalg.norm(x, ord=1, axis=-1) 18 | 19 | class HyperplaneDistance: 20 | def __init__(self, a=1, b=0): 21 | self.a = a 22 | self.norm_a = np.linalg.norm(a) 23 | self.b = b 24 | 25 | def __call__(self, x): 26 | return np.abs((self.a*x).sum(axis=-1) - self.b)/self.norm_a 27 | 28 | 29 | class SphereDistance: 30 | def __init__(self, r=1.): 31 | self.r = r 32 | 33 | def __call__(self, x): 34 | return np.abs(np.linalg.norm(x, axis=-1) - self.r) 35 | 36 | class QuadricDistance: 37 | def __init__(self, A = None, b = None, c = 0): 38 | self.A = A 39 | self.b = b 40 | self.c = c 41 | 42 | def __call__(self, x): 43 | return np.abs( 44 | (x * (x@self.A.T)).sum(axis=-1) + 45 | (x * self.b).sum(axis=-1) + 46 | self.c 47 | ) 48 | 49 | 50 | reg_func_dict = { 51 | 'L0': L0, 52 | 'L1': L1, 53 | 'Plane': HyperplaneDistance, 54 | 'Sphere': SphereDistance, 55 | 'Quadric': QuadricDistance 56 | } 57 | 58 | def select_reg_func(func): 59 | if isinstance(func, dict): 60 | return reg_func_dict[func['name']](**{k:v for (k,v) in func.items() if not k=='name'}) 61 | elif isinstance(func, str): 62 | return reg_func_dict[func]() 63 | else: 64 | return func 65 | 66 | 67 | 68 | #%% 69 | class regularize_objective: 70 | def __init__(self, f, reg_func, lamda = 1., lamda_broadcasted = False, p = 1): 71 | self.original_func = f 72 | self.reg_func = select_reg_func(reg_func) 73 | self.lamda = lamda 74 | self.lamda_broadcasted = lamda_broadcasted 75 | self.p = p 76 | 77 | def __call__(self, x): 78 | self.broadcast_lamda(x) 79 | return self.original_func(x) + self.lamda * self.reg_func(x) ** self.p 80 | 81 | def broadcast_lamda(self, x): 82 | if not self.lamda_broadcasted: 83 | M = x.shape[0] 84 | self.lamda = np.ones((M,1)) * self.lamda 85 | self.lamda_broadcasted = True 86 | 87 | @property 88 | def num_eval(self): 89 | return self.original_func.num_eval 90 | #%% 91 | def simple_param_rule_mean(dyn): 92 | return dyn.f.reg_func(dyn.x).mean(axis=-1) 93 | 94 | def weighted_param_rule_mean(dyn): 95 | E = np.exp(-dyn.alpha * dyn.eval_f(dyn.x)) 96 | R = dyn.f.reg_func(dyn.x) 97 | return (E * R).sum(axis=-1)/E.sum(axis=-1) 98 | 99 | 100 | class regularization_paramter_scheduler(param_update): 101 | def __init__( 102 | self, name: str ='lamda', 103 | theta = 1., 104 | theta_max = 1e5, 105 | factor_theta=1.1, 106 | factor_lamda=1.1, 107 | lamda_max = 1e15, 108 | rule_mean = 'weighted' 109 | ): 110 | super().__init__(name=name) 111 | self.theta = theta 112 | self.theta_max = theta_max 113 | self.factor_theta = factor_theta 114 | self.factor_lamda = factor_lamda 115 | self.param_rule_mean = weighted_param_rule_mean if rule_mean == 'weighted' else simple_param_rule_mean 116 | self.params_broadcasted = False 117 | self.lamda_max = lamda_max 118 | 119 | def broadcast_params(self, x): 120 | if not self.params_broadcasted: 121 | M = x.shape[0] 122 | self.theta = np.ones((M,)) * self.theta 123 | self.params_broadcasted = True 124 | 125 | def update(self, dyn): 126 | self.broadcast_params(dyn.x) 127 | m = self.param_rule_mean(dyn) 128 | t = (m <= 1/np.sqrt(self.theta)) 129 | self.theta[t] *= self.factor_theta 130 | self.theta[~t] = np.clip(self.theta[~t]/self.factor_theta, 131 | a_max= self.theta_max, a_min=None) 132 | dyn.f.lamda[~t] *= self.factor_lamda 133 | dyn.f.lamda = np.clip(dyn.f.lamda, a_max=self.lamda_max, a_min=0) 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/examples/success_evaluation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "093c184f-c3d4-4dd8-83b1-517cad4acf50", 6 | "metadata": {}, 7 | "source": [ 8 | "# Performance Evaluation\n", 9 | "\n", 10 | "In this notebook we explore the performance evaluation tools of ```cbxpy```. These are provided in the module ```cbx.utils.success```. For a test problem, we consider the Rastrigin function in dimension ```d=20```. We employ ```N=40``` particles and perform ```M=100``` runs." 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "id": "8e52898b-d5d9-40d2-ad82-c816430f926f", 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "name": "stdout", 21 | "output_type": "stream", 22 | "text": [ 23 | "The autoreload extension is already loaded. To reload it, use:\n", 24 | " %reload_ext autoreload\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "%load_ext autoreload\n", 30 | "%autoreload 2\n", 31 | "import cbx\n", 32 | "import cbx.dynamics as dyns\n", 33 | "import cbx.utils.success as success\n", 34 | "import numpy as np\n", 35 | "\n", 36 | "f = cbx.objectives.Rastrigin()\n", 37 | "M = 100\n", 38 | "N = 40\n", 39 | "d = 20\n", 40 | "\n", 41 | "x = np.random.uniform(-3,3,(M,N,d))\n", 42 | "kwargs = {'x':x, 'sigma':10.1, 'verbosity':0, 'max_it':5000, 'noise':'anisotropic', 'alpha':30, 'dt':0.01, 'f_dim':'3D'}\n", 43 | "x_true = np.zeros((x.shape[-1],))" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "f14c54bc-a412-4e37-93f4-4cee8d2fe243", 49 | "metadata": {}, 50 | "source": [ 51 | "## Perform optimization\n", 52 | "\n", 53 | "Using the keywords from above, we define a dynamic and run the optimization." 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 2, 59 | "id": "1e83d010-2d5f-42e0-9bd8-99ab619df68d", 60 | "metadata": { 61 | "scrolled": true 62 | }, 63 | "outputs": [], 64 | "source": [ 65 | "dyn = dyns.CBO(f,**kwargs)\n", 66 | "dyn.optimize();" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "id": "550d27a5-fa91-40e8-96e7-059b7c9205da", 72 | "metadata": {}, 73 | "source": [ 74 | "## Performance evaluation\n", 75 | "\n", 76 | "To evaluate the performance, we use the ```evaluation``` class, which accepts a list of performance criteria. Each criterion should have a call function that accepts a dynamic as the input and outputs a dictionary containing the following key-value pairs:\n", 77 | "\n", 78 | "* ```rate```: the success rate,\n", 79 | "* ```num```: the absolute number of successful runs,\n", 80 | "* ```idx```: the indices of the successful runs.\n", 81 | "\n", 82 | "We can directly apply this to our dynamic to get a result. Here we use the criterion ```dist_to_min```, which needs the true minimum as the input. This criterion will be satisfied if\n", 83 | "\n", 84 | "$$\\|x_{\\text{true}} - x\\|_p < \\texttt{tol},$$\n", 85 | "\n", 86 | "where the tolerance and $p$ of the norm can also be specified." 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 3, 92 | "id": "bfca0359-1c09-4c17-8ac5-f36539b2b542", 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "name": "stdout", 97 | "output_type": "stream", 98 | "text": [ 99 | "------------------------------\n", 100 | "Results of success evaluation:\n", 101 | "Success Rate: 1.0\n", 102 | "Succesful runs: 100\n", 103 | "Succesful idx: [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23\n", 104 | " 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47\n", 105 | " 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71\n", 106 | " 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95\n", 107 | " 96 97 98 99]\n" 108 | ] 109 | } 110 | ], 111 | "source": [ 112 | "seval = success.evaluation(criteria = [success.dist_to_min(x_true, tol=0.25, p=float('inf'))])\n", 113 | "seval(dyn);" 114 | ] 115 | } 116 | ], 117 | "metadata": { 118 | "kernelspec": { 119 | "display_name": "Python 3", 120 | "language": "python", 121 | "name": "python3" 122 | }, 123 | "language_info": { 124 | "codemirror_mode": { 125 | "name": "ipython", 126 | "version": 3 127 | }, 128 | "file_extension": ".py", 129 | "mimetype": "text/x-python", 130 | "name": "python", 131 | "nbconvert_exporter": "python", 132 | "pygments_lexer": "ipython3", 133 | "version": "3.12.4" 134 | } 135 | }, 136 | "nbformat": 4, 137 | "nbformat_minor": 5 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![cbx](https://github.com/PdIPS/CBXpy/assets/44805883/a5b96135-1039-4303-9cb1-32a1b1393fe3) 2 | 3 | [![status](https://joss.theoj.org/papers/008799348e8232eb9fe8180712e2dfb8/status.svg)](https://joss.theoj.org/papers/008799348e8232eb9fe8180712e2dfb8) 4 | ![tests](https://github.com/PdIPS/CBXpy/actions/workflows/Tests.yml/badge.svg) 5 | [![codecov](https://codecov.io/gh/PdIPS/CBXpy/graph/badge.svg?token=TU3LO8SLFP)](https://codecov.io/gh/PdIPS/CBXpy) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![Doc](https://img.shields.io/badge/Documentation-latest-blue)](https://pdips.github.io/CBXpy) 8 | 9 | A Python package for consensus-based particle dynamics, focusing on **optimization** and **sampling**. 10 | 11 | # How to use CBXPy? 12 | 13 | Minimizing a function using CBXPy can be done as follows: 14 | 15 | ```python 16 | from cbx.dynamics import CBO # import the CBO class 17 | 18 | f = lambda x: x[0]**2 + x[1]**2 # define the function to minimize 19 | x = CBO(f, d=2).optimize() # run the optimization 20 | ``` 21 | 22 | A documentation together with more examples and usage instructions is available at [https://pdips.github.io/CBXpy](https://pdips.github.io/CBXpy). 23 | 24 | 25 | # Installation 26 | 27 | Currently ```CBXPy``` can only be installed from PyPI with pip. 28 | 29 | ```bash 30 | pip install cbx 31 | ``` 32 | 33 | 34 | 35 | # What is CBX? 36 | 37 | Originally designed for optimization problems of the form 38 | 39 | $$ 40 | \min_{x \in \mathbb{R}^n} f(x), 41 | $$ 42 | 43 | the scheme was introduced as CBO (Consensus-Based Optimization). Given an ensemble of points $x = (x_1, \ldots, x_N)$, the update reads 44 | 45 | $$ 46 | x_i \gets x_i - \lambda\ dt\ (x_i - c(x)) + \sigma\ \sqrt{dt} |x_i - c(x)|\ \xi_i 47 | $$ 48 | 49 | where $\xi_i$ are i.i.d. standard normal random vectors. The core element is the consensus point 50 | 51 | $$ 52 | \begin{align*} 53 | c(x) = \left(\sum_{i=1}^{N} x_i\ \exp(-\alpha\ f(x_i))\right)\bigg/\left(\sum_{i=1}^N \exp(-\alpha\ f(x_i))\right) 54 | \end{align*} 55 | $$ 56 | 57 | with a parameter $\alpha>0$. The scheme can be extended to sampling problems known as CBS, clustering problems and opinion dynamics, which motivates the acronym 58 | **CBX**, indicating the flexibility of the scheme. 59 | 60 | ## Functionality 61 | 62 | Among others, CBXPy currently implements 63 | 64 | * CBO (Consensus-Based Optimization) [[1]](#CBO) 65 | * CBS (Consensus-Based Sampling) [[2]](#CBS) 66 | * CBO with memory [[3]](#CBOMemory) 67 | * Batching schemes [[4]](#Batching) 68 | * Polarized CBO [[5]](#PolarizedCBO) 69 | * Mirror CBO [[6]](#MirrorCBO) 70 | * Adamized CBO [[7]](#AdamizedCBO) 71 | * Constrained CBO methods, including 72 | * Drift Correction [[8]](#DriftCorrection) 73 | * Regularization [[9]](#Regularization) 74 | * Hypersurface CBO [[10]](#HypersurfaceCBO) 75 | 76 | 77 | ## References 78 | 79 | [1] A consensus-based model for global optimization and its mean-field limit, Pinnau, R., Totzeck, C., Tse, O. and Martin, S., Mathematical Models and Methods in Applied Sciences 2017 80 | 81 | [2] Consensus-based sampling, Carrillo, J.A., Hoffmann, F., Stuart, A.M., and Vaes, U., Studies in Applied Mathematics 2022 82 | 83 | [3] Leveraging Memory Effects and Gradient Information in Consensus-Based Optimization: On Global Convergence in Mean-Field Law, Riedl, K., 2022 84 | 85 | [4] A consensus-based global optimization method for high dimensional machine learning problems, Carrillo, J.A., Jin, S., Li, L. and Zhu, Y., ESAIM: Control, Optimisation and Calculus of Variations 2021 86 | 87 | [5] Bungert, L., Roith, T., & Wacker, P. (2024). Polarized consensus-based dynamics for optimization and sampling. Mathematical Programming, 1-31. 88 | 89 | [6] Bungert, L., Hoffmann, F., Kim, D. Y., & Roith, T. (2025). MirrorCBO: A consensus-based optimization method in the spirit of mirror descent. arXiv preprint arXiv:2501.12189. 90 | 91 | [7] Chen, J., Jin, S., & Lyu, L. (2020). A consensus-based global optimization method with adaptive momentum estimation. arXiv preprint arXiv:2012.04827. 92 | 93 | [8] Carrillo, J. A., Jin, S., Zhang, H., & Zhu, Y. (2024). An interacting particle consensus method for constrained global optimization. arXiv preprint arXiv:2405.00891. 94 | 95 | [9] Borghi, G., Herty, M., & Pareschi, L. (2023). Constrained consensus-based optimization. SIAM Journal on Optimization, 33(1), 211-236. 96 | 97 | [10] Fornasier, M., Huang, H., Pareschi, L., & Sünnen, P. (2020). Consensus-based optimization on hypersurfaces: Well-posedness and mean-field limit. Mathematical Models and Methods in Applied Sciences, 30(14), 2725-2751. 98 | -------------------------------------------------------------------------------- /cbx/utils/termination.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | #%% 4 | term_dict = {} 5 | #%% 6 | class energy_tol_term: 7 | """ 8 | Check if the energy is below a certain tolerance. 9 | 10 | Returns: 11 | bool: True if the energy is below the tolerance, False otherwise. 12 | """ 13 | def __init__(self, energy_tol=1e-7): 14 | self.energy_tol = energy_tol 15 | 16 | def __call__(self, dyn): 17 | return dyn.f_min < self.energy_tol 18 | 19 | term_dict.update( 20 | dict.fromkeys(['energy-tol', 'energy tol', 'energy_tol'], 21 | energy_tol_term 22 | ) 23 | ) 24 | 25 | class diff_tol_term: 26 | """ 27 | Checks if the update difference is less than the difference tolerance. 28 | 29 | Returns: 30 | bool: True if the update difference is less than the difference tolerance, False otherwise. 31 | """ 32 | def __init__(self, diff_tol=1e-7): 33 | self.diff_tol = diff_tol 34 | def __call__(self, dyn): 35 | return dyn.update_diff < self.diff_tol 36 | 37 | term_dict.update( 38 | dict.fromkeys(['diff-tol', 'diff tol', 'diff_tol'], 39 | diff_tol_term 40 | ) 41 | ) 42 | 43 | class max_eval_term: 44 | """ 45 | Check if the number of function evaluations is greater than or equal to the maximum number of evaluations. 46 | 47 | Returns: 48 | bool: True if the number of function evaluations is greater than or equal to the maximum number of evaluations, False otherwise. 49 | """ 50 | def __init__(self, max_eval=1000): 51 | self.max_eval = max_eval 52 | 53 | def __call__(self, dyn): 54 | estimated_eval = dyn.batch_size 55 | return (dyn.num_f_eval + estimated_eval) > self.max_eval 56 | 57 | term_dict.update( 58 | dict.fromkeys(['max-eval', 'max eval', 'max_eval'], 59 | max_eval_term 60 | ) 61 | ) 62 | 63 | class max_it_term: 64 | """ 65 | Checks if the current value of `dyn.it` is greater than or equal to the value of `dyn.max_it`. 66 | 67 | Returns: 68 | bool: True if `dyn.it` is greater than or equal to `dyn.max_it`, False otherwise. 69 | """ 70 | def __init__(self, max_it=1000): 71 | self.max_it = max_it 72 | 73 | def __call__(self, dyn): 74 | if self.max_it is None: 75 | return np.zeros((dyn.M), dtype=bool) 76 | else: 77 | return (dyn.it >= self.max_it) * np.ones((dyn.M), dtype=bool) 78 | 79 | term_dict.update( 80 | dict.fromkeys(['max-it', 'max it', 'max_it'], 81 | max_it_term 82 | ) 83 | ) 84 | 85 | class max_time_term: 86 | """ 87 | Checks if the current value of `dyn` is greater than or equal to the value of `dyn.max_time`. 88 | 89 | Returns: 90 | bool: True if `dyn.t` is greater than or equal to `dyn.max_time`, False otherwise. 91 | """ 92 | def __init__(self, max_time=10.): 93 | self.max_time = max_time 94 | def __call__(self, dyn): 95 | return (dyn.t >= self.max_time) * np.ones((dyn.M), dtype=bool) 96 | 97 | term_dict.update( 98 | dict.fromkeys(['max-time', 'max time', 'max_time'], 99 | max_time_term 100 | ) 101 | ) 102 | 103 | #%% 104 | class energy_stagnation_term: 105 | """ 106 | Checks if the loss was moving during the last iterations. 107 | """ 108 | def __init__(self, patience=20, std_thresh=1e-9): 109 | self.patience = patience 110 | self.losses = None 111 | self.std_thresh = std_thresh 112 | 113 | def __call__(self, dyn): 114 | if self.losses is None: 115 | self.losses = np.random.uniform(0., 1., size=(self.patience, dyn.M)) 116 | if dyn.consensus is None: 117 | return np.zeros((dyn.M), dtype=bool) 118 | # eval loss 119 | E = dyn.f(dyn.consensus[dyn.active_runs_idx, ...]) 120 | dyn.num_f_eval[dyn.active_runs_idx] += 1 121 | # update losses 122 | self.losses[dyn.it%self.patience, dyn.active_runs_idx] = E 123 | return np.std(self.losses, axis=0) < self.std_thresh 124 | 125 | term_dict.update( 126 | dict.fromkeys(['energy-stagnation', 'energy stagnation', 'energy_stagnation'], 127 | energy_stagnation_term 128 | ) 129 | ) 130 | #%% 131 | def select_term(term): 132 | if isinstance(term, str): 133 | return term_dict[term]() 134 | elif hasattr(term, 'keys'): 135 | if 'name' in term.keys(): 136 | return term_dict[term['name']]( 137 | **{k:v for k,v in term.items() if k not in ['name']} 138 | ) 139 | else: 140 | raise ValueError('The given term dict: ' + str(term) + '\n ' + 141 | 'does not have the necessary key ' + 142 | '"name"') 143 | else: 144 | return term -------------------------------------------------------------------------------- /cbx/utils/history.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from warnings import warn 3 | 4 | 5 | #%% 6 | class track: 7 | """ 8 | Base class for tracking of variables in the history dictionary of given dynamics. 9 | """ 10 | 11 | @staticmethod 12 | def init_history(dyn) -> None: 13 | """ 14 | Initializes the value to be tracked in the history dictionary of the given dyn object. 15 | 16 | Parameters 17 | ---------- 18 | dyn : object 19 | The object to track in the history dictionary. 20 | 21 | Returns 22 | ------- 23 | None 24 | """ 25 | pass 26 | 27 | @staticmethod 28 | def update(dyn) -> None: 29 | """ 30 | Updates the value to be tracked in the history dictionary of the given dyn object. 31 | 32 | Parameters 33 | ---------- 34 | dyn : object 35 | The object to track in the history dictionary. 36 | 37 | Returns 38 | ------- 39 | None 40 | """ 41 | pass 42 | 43 | #%% 44 | class default_track(track): 45 | def __init__(self, name): 46 | self.name = str(name) 47 | self.tracking = True 48 | 49 | def init_history(self, dyn) -> None: 50 | dyn.history[self.name] = [] 51 | 52 | 53 | def update(self, dyn) -> None: 54 | if self.tracking: 55 | if hasattr(dyn, self.name): 56 | dyn.history[self.name].append( 57 | dyn.copy(getattr(dyn, self.name)) 58 | ) 59 | else: 60 | warn('The tracker tried to track the variable ' + 61 | self.name + 62 | ' which is not an attribute of the given dynamic. ' + 63 | 'The varibale will not be tracked!', 64 | stacklevel=2) 65 | self.tracking = False 66 | 67 | #%% 68 | class track_x(track): 69 | """ 70 | Class for tracking of variable 'x' in the history dictionary. 71 | """ 72 | @staticmethod 73 | def init_history(dyn) -> None: 74 | dyn.history['x'] = [] 75 | dyn.history['x'].append(dyn.x) 76 | 77 | @staticmethod 78 | def update(dyn) -> None: 79 | """ 80 | Update the history of the 'x' variable by copying the current particles to the next time step. 81 | 82 | Parameters 83 | ---------- 84 | dyn : object 85 | The object to track in the history dictionary. 86 | 87 | Returns 88 | ------- 89 | None 90 | """ 91 | dyn.history['x'].append(dyn.copy(dyn.x)) 92 | 93 | 94 | class track_update_norm(track): 95 | """ 96 | Class for tracking the 'update_norm' entry in the history. 97 | 98 | """ 99 | 100 | @staticmethod 101 | def init_history(dyn) -> None: 102 | dyn.history['update_norm'] = [] 103 | 104 | @staticmethod 105 | def update(dyn) -> None: 106 | """ 107 | Updates the 'update_norm' entry in the 'history' dictionary with the 'update_diff' value. 108 | 109 | Parameters: 110 | None 111 | 112 | Returns: 113 | None 114 | """ 115 | dyn.history['update_norm'].append(dyn.update_diff) 116 | 117 | 118 | class track_energy(track): 119 | """ 120 | Class for tracking the 'energy' entry in the history. 121 | """ 122 | 123 | @staticmethod 124 | def init_history(dyn): 125 | dyn.history['energy'] = [] 126 | 127 | @staticmethod 128 | def update(dyn) -> None: 129 | dyn.history['energy'].append(np.copy(dyn.energy))# always assumes energy is numpy 130 | 131 | 132 | class track_consensus(track): 133 | """ 134 | Class for tracking the 'consensus' entry in the dynamic. 135 | """ 136 | 137 | @staticmethod 138 | def init_history(dyn) -> None: 139 | dyn.history['consensus'] = [] 140 | @staticmethod 141 | def update(dyn) -> None: 142 | dyn.history['consensus'].append(dyn.copy(dyn.consensus)) 143 | 144 | class track_drift_mean(track): 145 | """ 146 | Class for tracking the 'drift_mean' entry in the history. 147 | """ 148 | 149 | @staticmethod 150 | def init_history(dyn) -> None: 151 | dyn.history['drift_mean'] = [] 152 | @staticmethod 153 | def update(dyn) -> None: 154 | dyn.history['drift_mean'].append(np.mean(np.abs(dyn.drift), axis=(-2,-1))) 155 | 156 | class track_drift(track): 157 | """ 158 | Class for tracking the 'drift' entry in the history. 159 | """ 160 | 161 | @staticmethod 162 | def init_history(dyn) -> None: 163 | dyn.history['drift'] = [] 164 | dyn.history['particle_idx'] = [] 165 | 166 | @staticmethod 167 | def update(dyn) -> None: 168 | dyn.history['drift'].append(dyn.drift) 169 | dyn.history['particle_idx'].append(dyn.particle_idx) -------------------------------------------------------------------------------- /tests/dynamics/test_pdyn.py: -------------------------------------------------------------------------------- 1 | import cbx 2 | from cbx.dynamics.pdyn import ParticleDynamic, CBXDynamic 3 | import pytest 4 | import numpy as np 5 | from test_abstraction import test_abstract_dynamic 6 | import scipy as scp 7 | 8 | class Test_pdyn(test_abstract_dynamic): 9 | 10 | @pytest.fixture 11 | def dynamic(self): 12 | return ParticleDynamic 13 | 14 | def test_term_crit_maxit(self, dynamic, f): 15 | '''Test termination criterion on max iteration''' 16 | dyn = dynamic(f, d=5, max_it=3) 17 | dyn.optimize() 18 | assert dyn.it == 3 19 | 20 | def test_no_given_x(self, dynamic, f): 21 | '''Test if x is correctly initialized''' 22 | dyn = dynamic(f, d=5, M=4, N=3) 23 | assert dyn.x.shape == (4,3,5) 24 | assert dyn.M == 4 25 | assert dyn.N == 3 26 | 27 | def test_given_x_1D(self, dynamic, f): 28 | '''Test if given x (1D) is correctly reshaped''' 29 | dyn = dynamic(f, x=np.zeros((7)), max_it=1) 30 | assert dyn.x.shape == (1,1,7) 31 | assert dyn.M == 1 32 | assert dyn.N == 1 33 | 34 | def test_given_x_2D(self, dynamic, f): 35 | '''Test if given x (2D) is correctly reshaped''' 36 | dyn = dynamic(f, x=np.zeros((5,7)), max_it=1) 37 | assert dyn.x.shape == (1,5,7) 38 | assert dyn.M == 1 39 | assert dyn.N == 5 40 | 41 | def test_opt_and_output(self, dynamic, f): 42 | '''Test if optimization history is correctly saved and output is correct''' 43 | dyn = dynamic(f, x = np.zeros((6,5,7)), 44 | max_it=10) 45 | x = dyn.optimize() 46 | assert x.shape == (6,7) 47 | assert dyn.x.shape == (6,5,7) 48 | 49 | def test_f_wrong_dims(self, dynamic): 50 | '''Test if f_dim raises error for wrong dimensions''' 51 | def f(x): return x 52 | f_dim = '1D' 53 | x = np.random.uniform(-1,1,(6,5,7)) 54 | 55 | with pytest.raises(ValueError): 56 | dynamic(f, x=x, f_dim=f_dim) 57 | 58 | def test_optimization_performance(self, f, dynamic, opt_kwargs): 59 | pass 60 | 61 | def test_torch_handling(self, f, dynamic): 62 | import torch 63 | from cbx.utils.torch_utils import to_torch_dynamic 64 | x = torch.randn(size=(6,25,3)) 65 | 66 | 67 | def g(x): 68 | return torch.sum(x, dim=-1)**2 69 | 70 | def norm_torch(x, axis, **kwargs): 71 | return torch.linalg.norm(x, dim=axis, **kwargs) 72 | 73 | dyn = to_torch_dynamic(dynamic)(g, 74 | f_dim = '3D', 75 | x=x, 76 | max_it=15,) 77 | dyn.optimize() 78 | assert dyn.x.shape == x.shape and (dyn.x is not x) 79 | 80 | 81 | def test_mat_sqrt(): 82 | M=6 83 | A = np.random.uniform(size=(M,7,7)) 84 | A = (A@A.transpose(0,2,1)) 85 | C = cbx.dynamics.pdyn.compute_mat_sqrt(A) 86 | CC = np.zeros_like(C) 87 | for m in range(M): 88 | CC[m,...] = scp.linalg.sqrtm(A[m,...]) 89 | assert np.allclose(C, CC) 90 | 91 | 92 | class Test_cbx(test_abstract_dynamic): 93 | @pytest.fixture 94 | def dynamic(self): 95 | return CBXDynamic 96 | 97 | def test_apply_cov_mat(self, f, dynamic): 98 | M=7 99 | d=4 100 | N=12 101 | dyn = dynamic(f, M=M, d=d, N=N, noise='covariance') 102 | A = np.random.uniform(size=(M,d,d)) 103 | dyn.Cov_sqrt = A.copy() 104 | 105 | z = np.random.uniform(low=-1.,high=1., size=(M,N,d)) 106 | 107 | g = np.zeros_like(z) 108 | for m in range(M): 109 | for n in range(N): 110 | g[m,n,:] = A[m,...]@z[m,n,:] 111 | 112 | gg = dyn.noise_callable.apply_cov_sqrt(dyn.Cov_sqrt,z) 113 | assert np.allclose(g, gg) 114 | 115 | def test_optimization_performance(self, f, dynamic, opt_kwargs): 116 | pass 117 | 118 | def test_update_cov(self, f, dynamic): 119 | M=7 120 | d=4 121 | N=12 122 | dyn = dynamic(f, M=M, d=d, N=N) 123 | dyn.compute_consensus() 124 | dyn.drift = dyn.x - dyn.consensus 125 | dyn.update_covariance() 126 | 127 | Cov = np.zeros((M,d,d)) 128 | for m in range(M): 129 | C = np.zeros((N, d,d)) 130 | denom = 0. 131 | for n in range(N): 132 | drift = dyn.x[m,n,:] - dyn.consensus[m, 0, :] 133 | factor = np.exp(-dyn.alpha[m] * f(dyn.x[m,n,:])) 134 | denom += factor 135 | C[n,...] = np.outer(drift,drift) * factor 136 | 137 | Cov[m,...] = np.sum(C,axis=0)/denom 138 | 139 | dyn_Cov = dyn.Cov_sqrt@dyn.Cov_sqrt 140 | assert np.allclose(dyn_Cov, Cov) 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /cbx/dynamics/cbo_memory.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Union 3 | #from scipy.special import logsumexp 4 | 5 | from .cbo import CBO, cbo_update 6 | 7 | #%% CBO_Memory 8 | class CBOMemory(CBO): 9 | r"""Consensus-based optimization with memory effects (CBOMemory) class 10 | 11 | This class implements the CBO algorithm with memory effects as described in [1]_ and [2]_. The algorithm 12 | is a particle dynamic algorithm that is used to minimize the objective function :math:`f(x)`. 13 | 14 | Parameters 15 | ---------- 16 | f : objective 17 | The objective function :math:`f(x)` of the system. 18 | x : array_like, shape (N, d) 19 | The initial positions of the particles. For a system of :math:`N` particles, the i-th row of this array ``x[i,:]`` 20 | represents the position :math:`x_i` of the i-th particle. 21 | y : array_like, shape (N, d) 22 | The initial positions of the particles. For a system of :math:`N` particles, the i-th row of this array ``y[i,:]`` 23 | represents the or an approximation of the historical best position :math:`y_i` of the i-th particle. 24 | dt : float, optional 25 | The parameter :math:`dt` of the system. The default is 0.1. 26 | alpha : float, optional 27 | The heat parameter :math:`\alpha` of the system. The default is 1.0. 28 | lamda : float, optional 29 | The decay parameter :math:`\lambda` of the system. The default is 1.0. 30 | noise : noise_model, optional 31 | The noise model that is used to compute the noise vector. The default is ``normal_noise(dt=0.1)``. 32 | sigma : float, optional 33 | The parameter :math:`\sigma` of the noise model. The default is 1.0. 34 | lamda_memory : float, optional 35 | The decay parameter :math:`\lambda_{\text{memory}}` of the system. The default is 1.0. 36 | sigma_memory : float, optional 37 | The parameter :math:`\sigma_{\text{memory}}` of the noise model. The default is 1.0. 38 | 39 | References 40 | ---------- 41 | .. [1] Grassi, S. & Pareschi, L. (2021). From particle swarm optimization to consensus based optimization: stochastic modeling and mean-field limit. 42 | Math. Models Methods Appl. Sci., 31(8):1625–1657. 43 | .. [2] Riedl, K. (2023). Leveraging memory effects and gradient information in consensus-based optimization: On global convergence in mean-field law. 44 | Eur. J. Appl. Math., XX (X), XX-XX. 45 | 46 | """ 47 | 48 | def __init__(self, 49 | f, 50 | lamda_memory: float = 0.4, 51 | sigma_memory: Union[float, None] = None, 52 | **kwargs) -> None: 53 | 54 | super(CBOMemory, self).__init__(f, **kwargs) 55 | 56 | self.lamda_memory = lamda_memory 57 | 58 | # init historical best positions of particles 59 | self.y = self.copy(self.x) 60 | 61 | if sigma_memory is None: 62 | self.sigma_memory = self.lamda_memory * self.sigma 63 | else: 64 | self.sigma_memory = sigma_memory 65 | 66 | self.energy = self.f(self.x) 67 | self.num_f_eval += np.ones(self.M, dtype=int) * self.N # update number of function evaluations 68 | self.ergexp = tuple([Ellipsis] + [None for _ in range(self.x.ndim-2)]) 69 | 70 | def pre_step(self,): 71 | # save old positions 72 | self.x_old = self.copy(self.x) # save old positions 73 | self.y_old = self.copy(self.y) # save old positions 74 | 75 | # set new batch indices 76 | self.set_batch_idx() 77 | 78 | def memory_step(self,): 79 | # add memory step, first define new drift 80 | self.drift = self.x[self.particle_idx] - self.y[self.particle_idx] 81 | self.x[self.particle_idx] += cbo_update( 82 | self.drift, self.lamda_memory, self.dt, 83 | self.sigma_memory, self.noise() 84 | ) 85 | 86 | def inner_step(self,) -> None: 87 | r"""Performs one step of the CBOMemory algorithm. 88 | 89 | Parameters 90 | ---------- 91 | None 92 | 93 | Returns 94 | ------- 95 | None 96 | 97 | """ 98 | 99 | # first perform regular cbo step 100 | self.cbo_step() 101 | self.memory_step() 102 | 103 | # evaluation of objective function on all particles 104 | energy_new = self.eval_f(self.x[self.particle_idx]) 105 | 106 | # historical best positions of particles 107 | self.y[self.particle_idx] += ( 108 | ((self.energy>energy_new)[self.ergexp]) * 109 | (self.x[self.particle_idx] - self.y[self.particle_idx]) 110 | ) 111 | self.energy = np.minimum(self.energy, energy_new) 112 | 113 | 114 | def compute_consensus(self,) -> None: 115 | r"""Updates the weighted mean of the particles. 116 | 117 | Parameters 118 | ---------- 119 | None 120 | 121 | Returns 122 | ------- 123 | None 124 | 125 | """ 126 | self.consensus, _ = self._compute_consensus( 127 | self.energy, self.y[self.consensus_idx], 128 | self.alpha[self.active_runs_idx, :] 129 | ) 130 | 131 | def update_best_cur_particle(self,) -> None: 132 | self.f_min = self.energy.min(axis=-1) 133 | self.f_min_idx = self.energy.argmin(axis=-1) 134 | 135 | self.best_cur_particle = self.x[np.arange(self.M), self.f_min_idx, :] 136 | self.best_cur_energy = self.energy[np.arange(self.M), self.f_min_idx] 137 | -------------------------------------------------------------------------------- /cbx/dynamics/hypersurfacecbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics import CBO 2 | import numpy as np 3 | from scipy.special import logsumexp as logsumexp_scp 4 | 5 | #%% 6 | class signedDistance: 7 | def get_proj_matrix(self, v): 8 | grad = self.grad(v) 9 | #outer = np.einsum('...i,...j->...ij',grad,grad) 10 | outer = grad[..., None,:]*grad[..., None] 11 | return np.eye(v.shape[-1]) - outer 12 | 13 | def proj_tangent_space(self, x, z): 14 | g = self.grad(x) 15 | return z - g * (g*z).sum(axis=-1, keepdims=True) 16 | 17 | 18 | class sphereDistance(signedDistance): 19 | def grad(self, v): 20 | return v/np.linalg.norm(v,axis=-1, keepdims=True) 21 | 22 | def Laplacian(self, v): 23 | d = v.shape[-1] 24 | return (d-1)/np.linalg.norm(v, axis=-1, keepdims=True) 25 | 26 | def proj(self, x): 27 | return x/np.linalg.norm(x, axis=-1, keepdims=True) 28 | 29 | class StiefelDistance(signedDistance): 30 | def __init__(self, nk= None): 31 | self.nk = nk 32 | 33 | def grad(self, v): 34 | return v 35 | 36 | def Laplacian(self, v): 37 | return (2*self.nk[0] - self.nk[1] - 1)/2 38 | 39 | def proj(self, x): 40 | U, _, Vh = np.linalg.svd(x.reshape(-1, self.nk[0], self.nk[1]), full_matrices=False) 41 | return (U@Vh).reshape(x.shape) 42 | 43 | def proj_tangent_space(self, x, z): 44 | X, Z = (y.reshape(-1, self.nk[0], self.nk[1]) for y in (x,z)) 45 | X = x.reshape(-1, self.nk[0], self.nk[1]) 46 | 47 | return (Z - 0.5 * X@(np.moveaxis(Z, -1,-2)@X + np.moveaxis(X, -1,-2)@Z)).reshape(x.shape) 48 | 49 | class planeDistance(signedDistance): 50 | def __init__(self, a=0, b=1.): 51 | self.a = a 52 | self.norm_a = np.linalg.norm(a, axis=-1) 53 | self.b = b 54 | 55 | def grad(self, v): 56 | return self.a/self.norm_a * np.ones_like(v) 57 | 58 | def Laplacian(self, v): 59 | return 0 60 | 61 | def proj(self, y): 62 | return y - ((self.a * y).sum(axis=-1, keepdims=True) - self.b)/(self.norm_a**2) * self.a 63 | 64 | 65 | sdf_dict = {'sphere': sphereDistance, 66 | 'plane': planeDistance, 67 | 'Stiefel': StiefelDistance 68 | } 69 | 70 | def get_sdf(sdf): 71 | if sdf is None: 72 | return sphereDistance() 73 | else: 74 | return sdf_dict[sdf['name']](**{k:v for k,v in sdf.items() if k not in ['name']}) 75 | 76 | 77 | def compute_consensus_rescale(energy, x, alpha): 78 | emin = np.min(energy, axis=-1, keepdims=True) 79 | weights = - alpha * (energy - emin) 80 | coeff_expan = tuple([Ellipsis] + [None for i in range(x.ndim-2)]) 81 | coeffs = np.exp(weights - logsumexp_scp(weights, axis=-1, keepdims=True))[coeff_expan] 82 | 83 | return (x * coeffs).sum(axis=1, keepdims=True), energy 84 | 85 | 86 | 87 | 88 | class HyperSurfaceCBO(CBO): 89 | ''' 90 | Implements Hypersurface CBO described in the following papers: 91 | [1] Massimo Fornasier, Hui Huang, Lorenzo Pareschi, Philippe Sünnen 92 | "Consensus-Based Optimization on Hypersurfaces: Well-Posedness and Mean-Field Limit" 93 | [2] Massimo Fornasier, Hui Huang, Lorenzo Pareschi, Philippe Sünnen 94 | "Consensus-Based Optimization on the Sphere: Convergence to Global Minimizers and Machine Learning" 95 | 96 | Parameters 97 | ---------- 98 | f : callable 99 | The objective function to be minimized. 100 | noise : str, optional 101 | Type of noise to be used, either 'isotropic' or 'anisotropic'. Default is 'isotropic'. 102 | sdf : dict or None, optional 103 | Signed distance function to be used for the hypersurface. If None, a sphere distance is used. 104 | The dictionary should contain the name of the distance function and any additional parameters. 105 | **kwargs : additional keyword arguments 106 | Additional parameters for the CBO algorithm, such as `lamda`, `sigma`, etc. 107 | ''' 108 | 109 | def __init__(self, f, noise = 'isotropic', sdf = None, **kwargs): 110 | super().__init__( 111 | f, 112 | compute_consensus = compute_consensus_rescale, 113 | noise = noise, 114 | **kwargs 115 | ) 116 | self.sdf = get_sdf(sdf) 117 | if noise == 'anisotropic': 118 | self.noise_constr = self.noise_constr_aniso 119 | else: 120 | self.noise_constr = self.noise_constr_iso 121 | 122 | def inner_step(self,): 123 | self.compute_consensus() # compute consensus, sets self.energy and self.consensus 124 | self.drift = self.x - self.consensus 125 | 126 | # compute relevant terms for update 127 | # dir_drift = self.x + self.dt * apply_proj(Px, self.consensus) 128 | # noise = self.sigma * apply_proj(Px, self.noise()) 129 | 130 | # perform addition before matrix multiplaction to save time 131 | drift_and_noise = ( 132 | self.x + 133 | self.sdf.proj_tangent_space( 134 | self.x, 135 | self.dt * self.consensus + 136 | self.sigma * self.noise()) 137 | ) 138 | 139 | x_tilde = drift_and_noise - self.noise_constr() 140 | self.x = self.sdf.proj(x_tilde) 141 | 142 | def noise_constr_iso(self,): 143 | return ( 144 | self.dt * self.sigma**2/2 * self.sdf.Laplacian(self.x) * 145 | np.linalg.norm(self.drift, axis=-1, keepdims=True)**2 * 146 | # note that in the implementation here: 147 | # https://github.com/PhilippeSu/KV-CBO/blob/master/v2/kvcbo/KuramotoIteration.m 148 | # norm(drift)**2 is used instead of drift**2 149 | #self.drift**2 * 150 | self.sdf.grad(self.x) 151 | ) 152 | 153 | def noise_constr_aniso(self,): 154 | return (self.dt * self.sigma**2)/2 * ( 155 | np.linalg.norm(self.drift, axis=-1, keepdims=True) ** 2 + 156 | self.drift ** 2 - 157 | 2 * np.linalg.norm(self.drift * self.x, axis=-1, keepdims=True)**2 158 | ) * self.x -------------------------------------------------------------------------------- /cbx/utils/resampling.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Callable, List 3 | 4 | def apply_resampling_default(dyn, idx, sigma_indep=0.1, var_name='x'): 5 | z = dyn.sampler(size=(len(idx), dyn.N, *dyn.d)) 6 | getattr(dyn, var_name)[idx, ...] += sigma_indep * np.sqrt(dyn.dt) * z 7 | 8 | class resampling: 9 | """ 10 | Resamplings from a list of callables 11 | 12 | Parameters 13 | ---------- 14 | resamplings: list 15 | The list of resamplings to apply. Each entry should be a callable that accepts exactly one argument (the dynamic object) and returns a one-dimensional 16 | numpy array of indices. 17 | 18 | apply: Callable 19 | - ``dyn``: The dynmaic which the resampling is applied to. 20 | - ``idx``: List of indices that are resampled. 21 | 22 | The function that should be performed on a given dynamic for selected indices. This function has to have the signature apply(dyn,idx). 23 | 24 | """ 25 | def __init__(self, 26 | resamplings: List[Callable], 27 | apply:Callable = None, 28 | sigma_indep:float = 0.1, 29 | var_name = 'x', 30 | track_best_consensus = False 31 | ): 32 | self.resamplings = resamplings 33 | self.num_resampling = None 34 | self.apply = apply if apply is not None else apply_resampling_default 35 | self.sigma_indep = sigma_indep 36 | self.var_name = var_name 37 | self.track_best_consensus = track_best_consensus 38 | 39 | def __call__(self, dyn): 40 | """ 41 | Applies the resamplings to a given dynamic 42 | 43 | Parameters 44 | ---------- 45 | dyn 46 | The dynamic object to apply resamplings to 47 | 48 | Returns 49 | ------- 50 | None 51 | """ 52 | self.check_num_resamplings(dyn.M) 53 | idx = np.unique(np.concatenate([r(dyn) for r in self.resamplings])) 54 | if len(idx)>0: 55 | self.apply(dyn, idx, var_name = self.var_name, sigma_indep = self.sigma_indep) 56 | self.num_resampling[idx] += 1 57 | if dyn.verbosity > 0: 58 | print('Resampled in runs ' + str(idx)) 59 | 60 | self._track_best_consensus(dyn, idx) 61 | 62 | def check_num_resamplings(self, M): 63 | if self.num_resampling is None: 64 | self.num_resampling = np.zeros(shape=(M)) 65 | 66 | def _track_best_consensus(self, dyn, idx): 67 | if not self.track_best_consensus: 68 | return 69 | consensus_energy = dyn.eval_f(dyn.consensus[idx, ...]) 70 | 71 | if not hasattr(self, 'best_consensus'): 72 | self.best_consensus = dyn.copy(dyn.consensus) 73 | self.best_consensus_energy = np.inf * np.ones((dyn.M,1)) 74 | self.best_consensus_energy[idx, ...] = consensus_energy 75 | else: 76 | idx2 = np.where( 77 | consensus_energy < self.best_consensus_energy[idx, ...] 78 | )[0] 79 | self.best_consensus[idx[idx2], ...] = dyn.copy( 80 | dyn.consensus[idx[idx2], ...] 81 | ) 82 | 83 | class ensemble_update_resampling: 84 | """ 85 | Resampling based on ensemble update difference 86 | 87 | Parameters 88 | ---------- 89 | 90 | update_thresh: float 91 | The threshold for ensemble update difference. When the update difference is less than this threshold, the ensemble is resampled. 92 | 93 | Returns 94 | ------- 95 | 96 | The indices of the runs to resample as a numpy array. 97 | """ 98 | def __init__(self, update_thresh:float): 99 | self.update_thresh = update_thresh 100 | 101 | def __call__(self, dyn): 102 | return np.where(dyn.update_diff < self.update_thresh)[0] 103 | 104 | 105 | class consensus_stagnation: 106 | def __init__(self, patience=5, update_thresh=1e-4): 107 | self.patience = patience 108 | self.update_thresh = update_thresh 109 | self.consensus_updates = [] 110 | self.it = 0 111 | 112 | 113 | def __call__(self, dyn): 114 | return np.where(self.check_consensus_update(dyn) < self.update_thresh)[0] 115 | 116 | def check_consensus_update(self, dyn): 117 | self.it += 1 118 | wt = float('inf') * np.ones(dyn.M,) 119 | wt[dyn.active_runs_idx] = 0 120 | 121 | if hasattr(self, 'consensus_old'): 122 | diffs = np.zeros(dyn.M,) 123 | diffs[dyn.active_runs_idx] = dyn.to_numpy( 124 | dyn.norm(dyn.consensus - self.consensus_old[dyn.active_runs_idx, ...], 125 | axis=tuple(-i for i in range(1, dyn.x.ndim-1)) 126 | ) 127 | )[:, 0] 128 | 129 | self.consensus_updates.append(diffs) 130 | self.consensus_updates = self.consensus_updates[-self.patience:] 131 | wt += np.array(self.consensus_updates).max(axis=0) 132 | else: 133 | wt += float('inf') 134 | 135 | self.set_consensus_old(dyn) 136 | return wt 137 | 138 | def set_consensus_old(self, dyn): 139 | self.consensus_old = 0 * dyn.copy(dyn.x[:, 0:1, ...]) 140 | self.consensus_old[dyn.active_runs_idx, ...] = dyn.copy(dyn.consensus) 141 | 142 | class loss_update_resampling: 143 | """ 144 | Resampling based on loss update difference 145 | 146 | Parameters 147 | ---------- 148 | M: int 149 | The number of runs in the dynamic object the resampling is applied to. 150 | 151 | wait_thresh: int 152 | The number of iterations to wait before resampling. The default is 5. If the best loss is not updated after the specified number of 153 | iterations, the ensemble is resampled. 154 | 155 | Returns 156 | ------- 157 | 158 | The indices of the runs to resample as a numpy array. 159 | """ 160 | 161 | def __init__(self, wait_thresh:int = 5): 162 | self.wait_thresh = wait_thresh 163 | self.initalized = False 164 | 165 | def __call__(self, dyn): 166 | self.check_energy_wait(dyn.M) 167 | self.wait += 1 168 | u_idx = self.best_energy > dyn.best_energy 169 | self.wait[u_idx] = 0 170 | self.best_energy[u_idx] = dyn.best_energy[u_idx] 171 | idx = np.where(self.wait >= self.wait_thresh)[0] 172 | self.wait = np.mod(self.wait, self.wait_thresh) 173 | return idx 174 | 175 | def check_energy_wait(self, M): 176 | if not self.initalized: 177 | self.best_energy = float('inf') * np.ones((M,)) 178 | self.wait = np.zeros((M,), dtype=int) 179 | self.initalized = True 180 | 181 | -------------------------------------------------------------------------------- /cbx/dynamics/pso.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Union 3 | #from scipy.special import logsumexp 4 | 5 | from .pdyn import CBXDynamic 6 | 7 | #%% CBO_Memory 8 | class PSO(CBXDynamic): 9 | r"""Particle Swarm Optimization class 10 | 11 | This class implements the PSO algorithm as described in [1]_ and [2]_. The algorithm 12 | is a particle dynamic algorithm that is used to minimize the objective function :math:`f(x)`. 13 | 14 | Parameters 15 | ---------- 16 | f : objective 17 | The objective function :math:`f(x)` of the system. 18 | x : array_like, shape (N, d) 19 | The initial positions of the particles. For a system of :math:`N` particles, the i-th row of this array ``x[i,:]`` 20 | represents the position :math:`x_i` of the i-th particle. 21 | y : array_like, shape (N, d) 22 | The initial positions of the particles. For a system of :math:`N` particles, the i-th row of this array ``y[i,:]`` 23 | represents the or an approximation of the historical best position :math:`y_i` of the i-th particle. 24 | v : array_like, shape (N, d) 25 | The initial velocities of the particles. For a system of :math:`N` particles, the i-th row of this array ``y[i,:]`` 26 | represents the or an approximation of the historical best position :math:`y_i` of the i-th particle. 27 | dt : float, optional 28 | The parameter :math:`dt` of the system. The default is 0.1. 29 | alpha : float, optional 30 | The heat parameter :math:`\alpha` of the system. The default is 1.0. 31 | m : float, optional 32 | The inertia :math:`m` of the system. The default is 0.1. 33 | gamma : float, optional 34 | The friction coefficient :math:`\gamma` of the system. The default is 1-m. 35 | lamda : float, optional 36 | The decay parameter :math:`\lambda` of the system. The default is 1.0. 37 | noise : noise_model, optional 38 | The noise model that is used to compute the noise vector. The default is ``normal_noise(dt=0.1)``. 39 | sigma : float, optional 40 | The parameter :math:`\sigma` of the noise model. The default is 1.0. 41 | lamda_memory : float, optional 42 | The decay parameter :math:`\lambda_{\text{memory}}` of the system. The default is 1.0. 43 | sigma_memory : float, optional 44 | The parameter :math:`\sigma_{\text{memory}}` of the noise model. The default is 1.0. 45 | 46 | References 47 | ---------- 48 | .. [1] Grassi, S. & Pareschi, L. (2021). From particle swarm optimization to consensus based optimization: stochastic modeling and mean-field limit. 49 | Math. Models Methods Appl. Sci., 31(8):1625–1657. 50 | .. [2] Huang, H. & Qiu, J. & Riedl, K (2023). On the global convergence of particle swarm optimization methods. Appl. Math. Optim., 88(2):30. 51 | 52 | """ 53 | 54 | def __init__(self, 55 | f, 56 | m: float = 0.001, 57 | gamma: Union[float, None] = None, 58 | lamda_memory: float = 0.4, 59 | sigma_memory: Union[float, None] = None, 60 | **kwargs) -> None: 61 | 62 | super(PSO, self).__init__(f, **kwargs) 63 | 64 | self.m = m 65 | 66 | if gamma is None: 67 | self.gamma = 1 - m 68 | else: 69 | self.gamma = gamma 70 | 71 | self.lamda_memory = lamda_memory 72 | 73 | # init velocities of particles 74 | self.v = 0 * self.copy(self.x) 75 | 76 | # init historical best positions of particles 77 | self.y = self.copy(self.x) 78 | 79 | if sigma_memory is None: 80 | self.sigma_memory = self.lamda_memory * self.sigma 81 | else: 82 | self.sigma_memory = sigma_memory 83 | 84 | self.energy = self.f(self.x) 85 | self.num_f_eval += np.ones(self.M, dtype=int) * self.N # update number of function evaluations 86 | 87 | def pre_step(self,): 88 | # save old positions 89 | self.x_old = self.copy(self.x) # save old positions 90 | self.y_old = self.copy(self.y) # save old positions 91 | self.v_old = self.copy(self.v) # save old velocities 92 | 93 | # set new batch indices 94 | self.set_batch_idx() 95 | 96 | def inner_step(self,) -> None: 97 | r"""Performs one step of the PSO algorithm. 98 | 99 | Parameters 100 | ---------- 101 | None 102 | 103 | Returns 104 | ------- 105 | None 106 | 107 | """ 108 | 109 | mind = self.consensus_idx 110 | ind = self.particle_idx 111 | # first update 112 | self.consensus = self.compute_consensus(self.y[mind], self.energy[mind]) 113 | consensus_drift = self.x[ind] - self.consensus 114 | memory_drift = self.x[ind] - self.y[ind] 115 | 116 | # perform noise steps 117 | # **NOTE**: noise always uses the ``drift`` attribute of the dynamic. 118 | # We first use the actual drift here and 119 | # then the memory difference 120 | self.drift = consensus_drift 121 | self.s_consensus = self.sigma * self.noise() 122 | self.drift = memory_drift 123 | self.s_memory = self.sigma_memory * self.noise() 124 | 125 | # dynamcis update 126 | # velocities of particles 127 | self.v[ind] = ( 128 | self.m * self.dt * self.v[ind] + 129 | self.correction(self.lamda * self.dt * consensus_drift) + 130 | self.lamda_memory * self.dt * memory_drift + 131 | self.s_consensus + 132 | self.s_memory)/(self.m+self.gamma*self.dt) 133 | 134 | # momentaneous positions of particles 135 | self.x[ind] = self.x[ind] + self.dt * self.v[ind] 136 | 137 | # evaluation of objective function on all particles 138 | energy_new = self.f(self.x[ind]) 139 | self.num_f_eval += np.ones(self.M, dtype =int) * self.x[ind].shape[-2] # update number of function evaluations 140 | 141 | # historical best positions of particles 142 | energy_expand = tuple([Ellipsis] + [None for _ in range(self.x.ndim-2)]) 143 | self.y[ind] = self.y[ind] + ((self.energy>energy_new)[energy_expand]) * (self.x[ind] - self.y[ind]) 144 | self.energy = np.minimum(self.energy, energy_new) 145 | 146 | 147 | def compute_consensus(self, x_batch, energy) -> None: 148 | r"""Updates the weighted mean of the particles. 149 | 150 | Parameters 151 | ---------- 152 | None 153 | 154 | Returns 155 | ------- 156 | None 157 | 158 | """ 159 | c, _ = self._compute_consensus(energy, self.x[self.consensus_idx], self.alpha[self.active_runs_idx, :]) 160 | return c 161 | 162 | -------------------------------------------------------------------------------- /tests/dynamics/test_cbo.py: -------------------------------------------------------------------------------- 1 | from cbx.dynamics.cbo import CBO 2 | import pytest 3 | import numpy as np 4 | from test_abstraction import test_abstract_dynamic 5 | from cbx.utils.termination import max_it_term, energy_tol_term, max_eval_term, max_time_term 6 | 7 | class Test_cbo(test_abstract_dynamic): 8 | 9 | @pytest.fixture 10 | def dynamic(self): 11 | return CBO 12 | 13 | def test_term_crit_energy(self, dynamic, f): 14 | '''Test termination criterion on energy''' 15 | dyn = dynamic(f, x=np.zeros((3,5,7)), term_criteria=[energy_tol_term(1e-6), max_it_term(10)]) 16 | dyn.optimize() 17 | assert dyn.it == 1 18 | 19 | def test_term_crit_maxtime(self, dynamic, f): 20 | '''Test termination criterion on max time''' 21 | dyn = dynamic(f, d=5, term_criteria=[max_time_term(0.1)], dt=0.02) 22 | dyn.optimize() 23 | assert dyn.t == 0.1 24 | 25 | def test_term_crit_maxeval(self, dynamic, f): 26 | '''Test termination criterion on max function evaluations''' 27 | dyn = dynamic(f, d=5, M=4, N=3, term_criteria=[max_eval_term(6), max_it_term(10)]) 28 | dyn.optimize() 29 | assert np.all(dyn.num_f_eval == np.array([6,6,6,6])) 30 | 31 | def test_term_crit_maxeval_below(self, dynamic, f): 32 | '''Test termination criterion on max function evaluations''' 33 | dyn = dynamic(f, d=5, M=4, N=3, batch_args = {'size':2}, 34 | term_criteria=[max_eval_term(5), max_it_term(10)], 35 | check_f_dims = False) 36 | dyn.optimize() 37 | assert np.all(dyn.num_f_eval == np.array([4,4,4,4])) 38 | 39 | def test_mean_compute(self, dynamic, f): 40 | '''Test if mean is correctly computed''' 41 | x = np.random.uniform(-1,1,(3,5,7)) 42 | dyn = dynamic(f, x=x) 43 | dyn.step() 44 | mean = np.zeros((3,1,7)) 45 | 46 | for j in range(x.shape[0]): 47 | loc_mean = 0 48 | loc_denom = 0 49 | for i in range(x.shape[1]): 50 | loc_mean += np.exp(-dyn.alpha[j] * f(x[j,i,:])) * x[j,i,:] 51 | loc_denom += np.exp(-dyn.alpha[j] * f(x[j,i,:])) 52 | mean[j,...] = loc_mean / loc_denom 53 | 54 | assert np.allclose(dyn.consensus, mean) 55 | 56 | def test_mean_compute_batched(self, dynamic, f): 57 | '''Test if batched mean is correctly computed''' 58 | x = np.random.uniform(-1,1,(3,5,7)) 59 | dyn = dynamic(f, x=x, batch_args={'size':2}) 60 | dyn.step() 61 | mean = np.zeros((3,1,7)) 62 | ind = dyn.consensus_idx[1] 63 | 64 | for j in range(x.shape[0]): 65 | loc_mean = 0 66 | loc_denom = 0 67 | for i in range(ind.shape[1]): 68 | loc_mean += np.exp(-dyn.alpha[j] * f(x[j,ind[j, i],:])) * x[j,ind[j, i],:] 69 | loc_denom += np.exp(-dyn.alpha[j] * f(x[j,ind[j, i],:])) 70 | mean[j,...] = loc_mean / loc_denom 71 | 72 | assert np.allclose(dyn.consensus, mean) 73 | 74 | def test_step(self, dynamic, f): 75 | '''Test if step is correctly performed''' 76 | x = np.random.uniform(-1,1,(3,5,7)) 77 | delta = np.random.uniform(-1,1,(3,5,7)) 78 | def noise(dyn): 79 | return delta 80 | 81 | dyn = dynamic(f, x=x, noise=noise) 82 | dyn.step() 83 | x_new = x - dyn.lamda * dyn.dt * (x - dyn.consensus) + dyn.sigma * delta 84 | assert np.allclose(dyn.x, x_new) 85 | 86 | def test_step_batched(self, dynamic, f): 87 | '''Test if batched step is correctly performed''' 88 | x = np.random.uniform(-1,1,(3,5,7)) 89 | delta = np.random.uniform(-1,1,(3,5,7)) 90 | def noise(dyn): 91 | return delta 92 | 93 | dyn = dynamic(f, x=x, noise=noise, batch_args={'size':2, 'partial':False}) 94 | dyn.step() 95 | x_new = x - dyn.lamda * dyn.dt * (x - dyn.consensus) + dyn.sigma * delta 96 | assert np.allclose(dyn.x, x_new) 97 | 98 | def test_step_batched_partial(self, dynamic, f): 99 | '''Test if partial batched step is correctly performed''' 100 | x = np.random.uniform(-1,1,(3,5,7)) 101 | class noise: 102 | def __call__(self, dyn): 103 | self.s = dyn.sampler(size=dyn.drift.shape) * dyn.drift 104 | return self.s 105 | N = noise() 106 | 107 | dyn = dynamic(f, x=x, noise=N, batch_args={'size':2, 'partial':True}) 108 | dyn.step() 109 | ind = dyn.particle_idx[1] 110 | for j in range(x.shape[0]): 111 | for i in range(ind.shape[1]): 112 | x[j, ind[j,i], :] = x[j, ind[j,i], :] -\ 113 | dyn.lamda * dyn.dt * (x[j, ind[j,i], :] - dyn.consensus[j, 0, :])\ 114 | + dyn.sigma * N.s[j,i,:] 115 | 116 | assert np.allclose(dyn.x, x) 117 | 118 | def test_torch_handling(self, f, dynamic): 119 | '''Test if torch is correctly handled''' 120 | import torch 121 | from cbx.utils.torch_utils import to_torch_dynamic 122 | x = torch.randn(size=(6,25,3)) 123 | 124 | 125 | def g(x): 126 | return torch.sum(x, dim=-1)**2 127 | 128 | def norm_torch(x, axis, **kwargs): 129 | return torch.linalg.norm(x, dim=axis, **kwargs) 130 | 131 | dyn = to_torch_dynamic(dynamic)(g, 132 | f_dim = '3D', 133 | x=x, 134 | max_it=15,) 135 | dyn.optimize() 136 | assert dyn.x.shape == x.shape and (dyn.x is not x) 137 | 138 | def test_update_best_cur_particle(self, f, dynamic): 139 | x = np.zeros((5,3,2)) 140 | x[0, :,:] = np.array([[0.,0.], [2.,1.], [4.,5.]]) 141 | x[1, :,:] = np.array([[8.,7.], [0.,1.], [2.,1.]]) 142 | x[2, :,:] = np.array([[2.,5.], [0.,0.5], [2.,1.]]) 143 | x[3, :,:] = np.array([[5.3,0.], [2.,1.], [0.,0.3]]) 144 | x[4, :,:] = np.array([[0.,3.], [2.,1.], [0.,1.]]) 145 | dyn = dynamic(f, x=x) 146 | dyn.step() 147 | best_cur_particle = np.array([[0.,0.], [0.,1.], [0.,0.5], [0.,0.3], [0.,1.]]) 148 | 149 | assert np.allclose(dyn.best_cur_particle, best_cur_particle) 150 | 151 | def test_anisotropic_noise(self, f, dynamic): 152 | dyn = dynamic(f, d=3, noise='anisotropic') 153 | dyn.step() 154 | s = dyn.noise() 155 | assert s.shape == dyn.x.shape 156 | 157 | def test_heavi_side(self, f, dynamic): 158 | dyn = dynamic(f, d=3, N=4, M=2, correction='heavi_side') 159 | dyn.step() 160 | assert dyn.x.shape == (2, 4, 3) 161 | 162 | def test_heavi_side_reg(self, f, dynamic): 163 | dyn = dynamic(f, d=3, N=4, M=2, correction='heavi_side_reg') 164 | dyn.step() 165 | assert dyn.x.shape == (2, 4, 3) -------------------------------------------------------------------------------- /cbx/constraints.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | #%% 4 | def solve_system(A, x): 5 | return np.linalg.solve(A, x[..., None])[..., 0] 6 | #%% 7 | class MultiConstraint: 8 | def __init__(self, constraints): 9 | self.constraints = [] if constraints is None else constraints 10 | 11 | def __call__(self, x): 12 | out = 0 13 | for c in self.constraints: 14 | out += c(x) 15 | return out 16 | 17 | def squared(self, x): 18 | out = 0 19 | for c in self.constraints: 20 | out += c.squared(x) 21 | return out 22 | 23 | def grad_squared_sum(self, x): 24 | out = np.zeros_like(x) 25 | for con in self.constraints: 26 | out += con.grad_squared(x) 27 | return out 28 | 29 | def hessian_squared_sum(self, x): 30 | out = np.zeros(x.shape + (x.shape[-1],)) 31 | for con in self.constraints: 32 | out += con.hessian_squared(x) 33 | return out 34 | 35 | def hessian_sum(self, x): 36 | out = np.zeros(x.shape + (x.shape[-1],)) 37 | for con in self.constraints: 38 | out += con.hessian(x) 39 | return out 40 | 41 | def call_times_hessian_sum(self, x): 42 | out = 0 43 | for c in self.constraints: 44 | out += c(x)[..., None, None] * c.hessian(x) 45 | return out 46 | 47 | def solve_Id_call_times_hessian(self, x, x_tilde, factor = 1.): 48 | out = 0 49 | if len(self.constraints) > 1: 50 | A = np.eye(x.shape[-1]) + factor * self.call_times_hessian_sum(x) 51 | return solve_system(A, x_tilde) 52 | elif len(self.constraints) == 1: 53 | return self.constraints[0].solve_Id_call_times_hessian(x, x_tilde, factor=factor) 54 | else: 55 | return 0 56 | return out 57 | 58 | def solve_Id_plus_call_grad(self, x, x_tilde, factor = 1.): 59 | if len(self.constraints) > 1: 60 | raise ValueError('This function is not supported for more than one ' + 61 | 'constraint') 62 | elif len(self.constraints) == 1: 63 | return self.constraints[0].solve_Id_plus_call_grad(x, x_tilde, factor=factor) 64 | else: 65 | return x_tilde 66 | 67 | 68 | def solve_Id_hessian_squared_sum(self, x, x_tilde, factor=1.): 69 | if len(self.constraints) > 1: 70 | A = np.eye(x.shape[-1]) + factor * self.G.hessian_squared_sum(x) 71 | return solve_system(A, x_tilde) 72 | elif len(self.constraints) == 1: 73 | return self.constraints[0].solve_Id_hessian_squared( 74 | x, 75 | x_tilde, 76 | factor = factor 77 | ) 78 | else: 79 | return 0 80 | 81 | class Constraint: 82 | def squared(self, x): 83 | return self(x)**2 84 | 85 | def grad_squared(self, x): 86 | return 2 * self(x)[..., None] * self.grad(x) 87 | 88 | def hessian_squared(self, x): 89 | grad = self.grad(x) 90 | outer = np.einsum('...i,...j->...ij', grad, grad) 91 | return 2 * (outer + self(x)[..., None, None] * self.hessian(x)) 92 | 93 | def call_times_hessian(self, x): 94 | return self(x)[..., None, None] * self.hessian(x) 95 | 96 | def solve_Id_call_times_hessian(self, x, x_tilde, factor=1.): 97 | A = ( 98 | np.eye(x.shape[-1]) + 99 | factor * self.call_times_hessian(x) 100 | ) 101 | return solve_system(A, x_tilde) 102 | 103 | def solve_Id_hessian_squared(self, x, x_tilde, factor=1.): 104 | A = np.eye(x.shape[-1]) + factor * self.hessian_squared(x) 105 | return solve_system(A, x_tilde) 106 | 107 | class NoConstraint(Constraint): 108 | def __init__(self,): 109 | super().__init__() 110 | 111 | def __call__(self, x): 112 | return np.zeros(x.shape[:-1]) 113 | 114 | def grad(self, x): 115 | return np.zeros_like(x) 116 | 117 | def hessian(self, x): 118 | return np.zeros(x.shape + (x.shape[-1],)) 119 | 120 | 121 | class quadricConstraint(Constraint): 122 | def __init__(self, A = None, b = None, c = 0): 123 | self.A = A 124 | self.b = b 125 | self.c = c 126 | 127 | def __call__(self, x): 128 | return ( 129 | (x * (x@self.A)).sum(axis=-1) + 130 | (x * self.b).sum(axis=-1) + 131 | self.c 132 | ) 133 | 134 | def grad(self, x): 135 | return 2 * x@self.A + self.b 136 | 137 | def hessian(self, x): 138 | return 2 * self.A 139 | 140 | def solve_Id_plus_call_grad(self, x, x_tilde, factor=1.): 141 | M = (np.eye(self.A.shape[0])[None, None, ...] + 142 | 4 * factor * self(x)[..., None, None] * self.A) 143 | return np.linalg.solve(M, (x_tilde - 2 * factor * self(x)[..., None] * self.b)[..., None])[..., 0] 144 | 145 | class sphereConstraint(Constraint): 146 | def __init__(self, r=1.): 147 | super().__init__() 148 | self.r = r 149 | 150 | def __call__(self, x): 151 | return np.linalg.norm(x, axis=-1)**2 - self.r 152 | 153 | def grad(self, x): 154 | return 2 * x 155 | 156 | def hessian(self, x): 157 | return 2 * np.tile(np.eye(x.shape[-1]), x.shape[:-1] + (1,1)) 158 | 159 | def solve_Id_plus_call_grad(self, x, x_tilde, factor=1.): 160 | return (1/(1 + 4 * factor * (np.linalg.norm(x, axis=-1, keepdims=True)**2 - self.r))) * x_tilde 161 | 162 | 163 | 164 | class planeConstraint(Constraint): 165 | def __init__(self, a=0, b=1.): 166 | super().__init__() 167 | self.a = a 168 | self.norm_a = np.linalg.norm(a, axis=-1) 169 | self.b = b 170 | 171 | def __call__(self, x): 172 | return ((self.a * x).sum(axis=-1) - self.b)/self.norm_a 173 | 174 | def grad(self, x): 175 | return (self.a/self.norm_a) * np.ones_like(x) 176 | 177 | def hessian(self, x): 178 | return np.zeros(x.shape + (x.shape[-1],)) 179 | 180 | def solve_Id_call_times_hessian(self, x, x_tilde, factor=1.): 181 | return x_tilde 182 | 183 | def solve_Id_plus_call_grad(self, x, x_tilde, factor=1.): 184 | return x_tilde - 2 * factor * self(x)[...,None] * self.a/self.norm_a 185 | 186 | 187 | const_dict = {'plane': planeConstraint, 188 | 'sphere': sphereConstraint, 189 | 'quadric': quadricConstraint} 190 | 191 | def get_constraints(const): 192 | CS = [] 193 | const = [] if const is None else const 194 | for c in const: 195 | if c is None: 196 | pass #return NoConstraint() 197 | else: 198 | CS.append(const_dict[c['name']](**{k:v for k,v in c.items() if k not in ['name']})) 199 | return CS -------------------------------------------------------------------------------- /cbx/utils/torch_utils.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from functools import wraps 3 | from ..scheduler import bisection_solve, eff_sample_size_gap 4 | import numpy as np 5 | 6 | try: # try torch import 7 | import torch 8 | from torch import logsumexp 9 | from torch.func import functional_call, stack_module_state, vmap 10 | except ImportError: 11 | _has_torch = False 12 | else: 13 | _has_torch = True 14 | 15 | 16 | #%% 17 | def requires_torch(f): 18 | @wraps(f) 19 | def torch_decorated(*args, **kwargs): 20 | if not _has_torch: 21 | raise ImportError('The requested function: ' + f.__name__ + 'requires ' + 22 | 'torch, but torch import failed! Either install torch manually ' + 23 | 'or install cbx with torch option: pip install cbx[torch]') 24 | return f(*args, **kwargs) 25 | 26 | return torch_decorated 27 | 28 | @requires_torch 29 | def norm_torch(x, axis, **kwargs): 30 | return torch.linalg.vector_norm(x, dim=axis, **kwargs) 31 | 32 | @requires_torch 33 | def compute_consensus_torch(energy, x, alpha): 34 | weights = - alpha * energy 35 | coeff_expan = tuple([Ellipsis] + [None for i in range(x.ndim-2)]) 36 | coeffs = torch.exp(weights - logsumexp(weights, dim=-1, keepdims=True))[coeff_expan] 37 | return (x * coeffs).sum(axis=1, keepdims=True), energy.cpu().numpy() 38 | 39 | @requires_torch 40 | def compute_polar_consensus_torch(energy, x, neg_log_eval, alpha = 1., kernel_factor = 1.): 41 | weights = -kernel_factor * neg_log_eval - alpha * energy[:,None,:] 42 | coeff_expan = tuple([Ellipsis] + [None for i in range(x.ndim-2)]) 43 | coeffs = torch.exp(weights - torch.logsumexp(weights, dim=(-1,), keepdims=True))[coeff_expan] 44 | c = torch.sum(x[:,None,...] * coeffs, axis=2) 45 | return c, energy.detach().cpu().numpy() 46 | 47 | @requires_torch 48 | def exponential_torch(device): 49 | def _exponential_torch(size=None): 50 | x = torch.distributions.Exponential(1.0).sample( 51 | size 52 | ) 53 | sign = torch.randint(0, 2, size) * 2 - 1 54 | x: torch.Tensor = x* sign 55 | return x.to(device) 56 | return _exponential_torch 57 | 58 | @requires_torch 59 | class post_process_default_torch: 60 | """ 61 | Default post processing. 62 | 63 | This function performs some operations on the particles, after the inner step. 64 | 65 | Parameters: 66 | None 67 | 68 | Return: 69 | None 70 | """ 71 | def __init__(self, max_thresh: float = 1e8): 72 | self.max_thresh = max_thresh 73 | 74 | def __call__(self, dyn): 75 | dyn.x = torch.nan_to_num(dyn.x, nan=self.max_thresh) 76 | dyn.x = torch.clamp(dyn.x, min=-self.max_thresh, max=self.max_thresh) 77 | 78 | @requires_torch 79 | def standard_normal_torch(device): 80 | def _normal_torch(size=None): 81 | return torch.randn(size=size).to(device) 82 | return _normal_torch 83 | 84 | def set_post_process_torch(self, post_process): 85 | self.post_process = post_process if post_process is not None else post_process_default_torch() 86 | 87 | def set_array_backend_funs_torch(self, copy, norm, sampler): 88 | self.copy = copy if copy is not None else torch.clone 89 | self.norm = norm if norm is not None else norm_torch 90 | self.sampler = sampler if sampler is not None else standard_normal_torch(self.device) 91 | 92 | def init_particles(self, shape=None,): 93 | return torch.zeros(size=shape).uniform_(-1., 1.) 94 | 95 | def init_consensus(self, compute_consensus): 96 | self.consensus = None #consensus point 97 | self._compute_consensus = compute_consensus if compute_consensus is not None else compute_consensus_torch 98 | 99 | def to_numpy(self, x): 100 | return x.detach().cpu().numpy() 101 | 102 | def to_torch_dynamic(dyn_cls): 103 | def add_device_init(self, *args, device='cpu', **kwargs): 104 | self.device = device 105 | dyn_cls.__init__(self, *args, **kwargs) 106 | 107 | return type(dyn_cls.__name__ + str('_torch'), 108 | (dyn_cls,), 109 | dict( 110 | __init__ = add_device_init, 111 | set_array_backend_funs=set_array_backend_funs_torch, 112 | init_particles=init_particles, 113 | init_consensus=init_consensus, 114 | set_post_process = set_post_process_torch, 115 | to_numpy = to_numpy 116 | ) 117 | ) 118 | 119 | 120 | @requires_torch 121 | def eval_model(x, model, w, pprop): 122 | params = {p: w[pprop[p][-2]:pprop[p][-1]].view(pprop[p][0]) for p in pprop} 123 | return functional_call(model, (params, {}), x) 124 | 125 | @requires_torch 126 | def eval_models(x, model, w, pprop): 127 | return vmap(eval_model, (None, None, 0, None))(x, model, w, pprop) 128 | 129 | @requires_torch 130 | def eval_loss(x, y, loss_fct, model, w, pprop): 131 | with torch.no_grad(): 132 | return loss_fct(eval_model(x, model, w, pprop), y) 133 | 134 | @requires_torch 135 | def eval_losses(x, y, loss_fct, model, w, pprop): 136 | return vmap(eval_loss, (None, None, None, None, 0, None))(x, y, loss_fct, model, w, pprop) 137 | 138 | @requires_torch 139 | def eval_acc(model, w, pprop, loader): 140 | res = 0 141 | num_img = 0 142 | for (x,y) in iter(loader): 143 | x = x.to(w.device) 144 | y = y.to(w.device) 145 | res += torch.sum(eval_model(x, model, w, pprop).argmax(axis=1)==y) 146 | num_img += x.shape[0] 147 | return res/num_img 148 | 149 | @requires_torch 150 | def flatten_parameters(models, pnames): 151 | params, buffers = stack_module_state(models) 152 | N = list(params.values())[-1].shape[0] 153 | return torch.concatenate([params[pname].view(N,-1).detach() for pname in pnames], dim=-1) 154 | 155 | @requires_torch 156 | def get_param_properties(models, pnames=None): 157 | params, buffers = stack_module_state(models) 158 | pnames = pnames if pnames is not None else params.keys() 159 | pprop = OrderedDict() 160 | for p in pnames: 161 | a = 0 162 | if len(pprop)>0: 163 | a = pprop[next(reversed(pprop))][-1] 164 | pprop[p] = (params[p][0,...].shape, a, a + params[p][0,...].numel()) 165 | return pprop 166 | 167 | @requires_torch 168 | class effective_sample_size: 169 | def __init__(self, name = 'alpha', eta=.5, maximum=1e5, minimum=1e-5, solve_max_it = 15): 170 | self.name = name 171 | self.eta = eta 172 | self.J_eff = 1.0 173 | self.solve_max_it = solve_max_it 174 | self.maximum = maximum 175 | self.minimum = minimum 176 | 177 | def update(self, dyn): 178 | val = getattr(dyn, self.name) 179 | device = val.device 180 | val = bisection_solve( 181 | eff_sample_size_gap(dyn.energy, self.eta), 182 | self.minimum * np.ones((dyn.M,)), self.maximum * np.ones((dyn.M,)), 183 | max_it = self.solve_max_it, thresh=1e-2 184 | ) 185 | setattr(dyn, self.name, torch.tensor(val[:, None], device=device, dtype = dyn.x.dtype)) -------------------------------------------------------------------------------- /cbx/scheduler.py: -------------------------------------------------------------------------------- 1 | r""" 2 | Scheduler 3 | ========== 4 | 5 | This module implements the schedulers employed in conensus based schemes. 6 | 7 | """ 8 | 9 | import numpy as np 10 | from scipy.special import logsumexp 11 | import warnings 12 | 13 | class param_update(): 14 | r"""Base class for parameter updates 15 | 16 | This class implements the base class for parameter updates. 17 | 18 | Parameters 19 | ---------- 20 | name : str 21 | The name of the parameter that should be updated. The default is 'alpha'. 22 | maximum : float 23 | The maximum value of the parameter. The default is 1e5. 24 | """ 25 | def __init__( 26 | self, 27 | name: str ='alpha', 28 | maximum: float = 1e5, 29 | minimum: float = 1e-5 30 | ): 31 | self.name = name 32 | self.maximum = maximum 33 | self.minimum = minimum 34 | 35 | def update(self, dyn) -> None: 36 | """ 37 | Updates the object with the given `dyn` parameter. 38 | 39 | Parameters 40 | ---------- 41 | dyn 42 | The dynamic of which the parameter should be updated. 43 | 44 | Returns 45 | ------- 46 | None 47 | """ 48 | pass 49 | 50 | def ensure_max(self, dyn): 51 | r"""Ensures that the parameter does not exceed its maximum value.""" 52 | setattr(dyn, self.name, np.minimum(self.maximum, getattr(dyn, self.name))) 53 | 54 | 55 | class scheduler(): 56 | r"""scheduler class 57 | 58 | This class allows to update multiple parmeters with one update call. 59 | 60 | Parameters 61 | ---------- 62 | var_params : list 63 | A list of parameter updates, that implement an ``update`` function. 64 | 65 | """ 66 | 67 | def __init__(self, var_params): 68 | self.var_params = var_params 69 | 70 | def update(self, dyn) -> None: 71 | """ 72 | Updates the dynamic variables in the object. 73 | 74 | Parameters 75 | ---------- 76 | dyn: The dynamic variables to update. 77 | 78 | Returns 79 | ------- 80 | None 81 | """ 82 | for var_param in self.var_params: 83 | var_param.update(dyn) 84 | 85 | class multiply(param_update): 86 | def __init__(self, 87 | factor = 1.0, 88 | **kwargs): 89 | """ 90 | This scheduler updates the parameter as specified by ``'name'``, by multiplying it by a given ``'factor'``. 91 | 92 | Parameters 93 | ---------- 94 | factor : float 95 | The factor by which the parameter should be multiplied. 96 | 97 | """ 98 | super(multiply, self).__init__(**kwargs) 99 | self.factor = factor 100 | 101 | def update(self, dyn) -> None: 102 | r"""Update the parameter as specified by ``'name'``, by multiplying it by a given ``'factor'``.""" 103 | old_val = getattr(dyn, self.name) 104 | new_val = self.factor * old_val 105 | setattr(dyn, self.name, new_val) 106 | self.ensure_max(dyn) 107 | 108 | 109 | # class for alpha_eff scheduler 110 | class effective_sample_size(param_update): 111 | r"""effective sample size scheduler class 112 | 113 | This class implements a scheduler for the :math:`\alpha`-parameter based on the effective sample size as inroduced 114 | in [1]_. In every step we try to find :math:`\alpha` such that 115 | The :math:`\alpha`-parameter is updated according to the rule 116 | 117 | .. math:: 118 | 119 | J_{eff}(\alpha) = \frac{\left(\sum_{i=1}^N w_i(\alpha)\right)^2}{\sum_{i=1}^N w_i(\alpha)^2} = \eta N 120 | 121 | where :math:`\eta` is a parameter, :math:`N` is the number of particles and :math:`w_i := \exp(-\alpha f(x_i))`. The above equation is solved via bisection. 122 | 123 | 124 | 125 | Parameters 126 | ---------- 127 | eta : float, optional 128 | The parameter :math:`\eta` of the scheduler. The default is 0.5. 129 | alpha_max : float, optional 130 | The maximum value of the :math:`\alpha`-parameter. The default is 100000.0. 131 | factor : float, optional 132 | The parameter :math:`r` of the scheduler. The default is 1.05. 133 | 134 | References 135 | ---------- 136 | .. [1] Carrillo, J. A., Hoffmann, F., Stuart, A. M., & Vaes, U. (2022). Consensus‐based sampling. Studies in Applied Mathematics, 148(3), 1069-1140. 137 | 138 | 139 | """ 140 | def __init__(self, name = 'alpha', eta=.5, maximum=1e5, solve_max_it = 15): 141 | super().__init__(name = name, maximum=maximum) 142 | if self.name != 'alpha': 143 | warnings.warn('effective_number scheduler only works for alpha parameter! You specified name = {}!'.format(self.name), stacklevel=2) 144 | self.eta = eta 145 | self.J_eff = 1.0 146 | self.solve_max_it = solve_max_it 147 | 148 | def update(self, dyn): 149 | val = getattr(dyn, self.name) 150 | val = bisection_solve( 151 | eff_sample_size_gap(dyn.energy, self.eta), 152 | self.minimum * np.ones((dyn.M,)), self.maximum * np.ones((dyn.M,)), 153 | max_it = self.solve_max_it, thresh=1e-2 154 | ) 155 | setattr(dyn, self.name, val[:, None]) 156 | self.ensure_max(dyn) 157 | 158 | 159 | class eff_sample_size_gap: 160 | r"""effective sample size gap 161 | 162 | This class is used for the effective sample size scheduler. Its call is defined as 163 | 164 | .. math:: 165 | 166 | \alpha \mapsto J_{eff}(\alpha) - \eta N. 167 | 168 | Therefore, the root of this non-increasing function solve the effective sampling size equation for :math:`\alpha`. 169 | """ 170 | 171 | 172 | def __init__(self, energy, eta): 173 | self.eta = eta 174 | self.energy = energy 175 | self.N = energy.shape[-1] 176 | 177 | def __call__(self, alpha): 178 | nom = logsumexp(-alpha[:, None] * self.energy, axis=-1) 179 | denom = logsumexp(-2 * alpha[:, None] * self.energy, axis=-1) 180 | return np.exp(2 * nom - denom) - self.eta * self.N 181 | 182 | def bisection_solve(f, low, high, max_it = 100, thresh = 1e-2, verbosity=0): 183 | r"""simple bisection optimization to solve for roots 184 | 185 | Parameters 186 | ---------- 187 | f : Callable 188 | A non-increasing function of which we want to find roots, it expects inputs of the shape (M,) where M denotes 189 | the number of different runs 190 | low: Array 191 | The low initial value for the bisection, should be an array of size (M,) 192 | high: Array 193 | The high initial value for the bisection, should be an array of size (M,) 194 | 195 | 196 | Returns 197 | ------- 198 | roots of the function f 199 | """ 200 | it = 0 201 | x = high 202 | term = False 203 | idx = np.arange(len(low)) 204 | while not term: 205 | x = (low + high)/2 206 | fx = f(x) 207 | gtzero = np.where(fx[idx] > 0)[0] 208 | ltzero = np.where(fx[idx] < 0)[0] 209 | # update low and high 210 | low[idx[gtzero]] = x[idx[gtzero]] 211 | high[idx[ltzero]] = x[idx[ltzero]] 212 | # update running idx and iteration 213 | idx = np.where(np.abs(fx) > thresh)[0] 214 | it += 1 215 | term = (it > max_it) | (len(idx) == 0) 216 | if verbosity > 0: 217 | print('Finishing after ' + str(it) + ' Iterations') 218 | return x -------------------------------------------------------------------------------- /cbx/noise.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements the noise methods for cbx dynamics 3 | """ 4 | from typing import Callable 5 | from numpy.typing import ArrayLike 6 | from numpy.random import normal 7 | import numpy as np 8 | 9 | def get_noise(name: str, dyn): 10 | if name == 'isotropic': 11 | return isotropic_noise(norm=dyn.norm, sampler=dyn.sampler) 12 | elif name == 'anisotropic': 13 | return anisotropic_noise(norm=dyn.norm, sampler=dyn.sampler) 14 | elif name == 'covariance' or name == 'sampling': 15 | return covariance_noise(norm=dyn.norm, sampler=dyn.sampler) 16 | elif name == 'constant': 17 | return constant_noise(norm=dyn.norm, sampler=dyn.sampler) 18 | else: 19 | raise NotImplementedError('Noise model {} not implemented'.format(name)) 20 | 21 | 22 | class noise: 23 | def __init__(self, 24 | norm: Callable = None, 25 | sampler: Callable = None): 26 | self.norm = norm if norm is not None else np.linalg.norm 27 | self.sampler = sampler if sampler is not None else normal 28 | def __call__(self, dyn): 29 | """ 30 | This function returns the noise vector for a given dynamic object. This is the function that is called in the dynamic object. 31 | Therfore, it has to have a fixed signature, accepting a dynamic object as input. The concrete implementation of this function 32 | is provided by each specific noise model. 33 | 34 | Parameters 35 | ---------- 36 | dyn 37 | The dynamic object 38 | 39 | Returns 40 | ------- 41 | ArrayLike 42 | The noise vector 43 | """ 44 | raise NotImplementedError('Base class does not implement __call__') 45 | 46 | def sample(self, ): 47 | """ 48 | This function performs the sampling of the noise vector. Each specific noise model must implement this function. 49 | """ 50 | raise NotImplementedError('Base class does not implement sample') 51 | 52 | class constant_noise(noise): 53 | def __init__(self, 54 | norm: Callable = None, 55 | sampler: Callable = None): 56 | super().__init__(norm = norm, sampler = sampler) 57 | 58 | def __call__(self, dyn) -> ArrayLike: 59 | return np.sqrt(dyn.dt) * self.sample(dyn.drift) 60 | 61 | def sample(self, drift) -> ArrayLike: 62 | return self.sampler(size = drift.shape) 63 | 64 | class isotropic_noise(noise): 65 | r""" 66 | 67 | This class implements the isotropic noise model. From the drift :math:`d = x - c(x)`, 68 | the noise vector is computed as 69 | 70 | .. math:: 71 | 72 | n_{m,n} = \sqrt{dt}\cdot \|d_{m,n}\|_2\cdot \xi. 73 | 74 | 75 | Here, :math:`\xi` is a random vector of size :math:`(d)` distributed according to :math:`\mathcal{N}(0,1)`. 76 | 77 | Parameters 78 | ---------- 79 | None 80 | 81 | Note 82 | ---- 83 | Only the norm of the drift is used for the noise. Therefore, the noise vector is scaled with the same factor in each dimension, 84 | which motivates the name **isotropic**. 85 | """ 86 | def __init__(self, 87 | norm: Callable = None, 88 | sampler: Callable = None): 89 | super().__init__(norm = norm, sampler = sampler) 90 | 91 | def __call__(self, dyn) -> ArrayLike: 92 | return np.sqrt(dyn.dt) * self.sample(dyn.drift) 93 | 94 | def sample(self, drift) -> ArrayLike: 95 | r''' 96 | 97 | This function implements the isotropic noise model. From the drift :math:`d = x - c(x)`, 98 | the noise vector is computed as 99 | 100 | .. math:: 101 | 102 | n_{m,n} = \sqrt{dt}\cdot \|d_{m,n}\|_2\cdot \xi. 103 | 104 | 105 | Here, :math:`\xi` is a random vector of size :math:`(d)` distributed according to :math:`\mathcal{N}(0,1)`. 106 | 107 | Parameters 108 | ---------- 109 | None 110 | 111 | Note 112 | ---- 113 | Only the norm of the drift is used for the noise. Therefore, the noise vector is scaled with the same factor in each dimension, 114 | which motivates the name **isotropic**. 115 | ''' 116 | z = self.sampler(size = drift.shape) 117 | return z * self.norm(drift.reshape(*drift.shape[:2],-1), axis=-1).reshape(drift.shape[:2] + (drift.ndim-2)*(1,)) 118 | 119 | 120 | 121 | class anisotropic_noise(noise): 122 | r""" 123 | This class implements the anisotropic noise model. From the drift :math:`d = x - c(x)`, 124 | the noise vector is computed as 125 | 126 | .. math:: 127 | 128 | n_{m,n} = \sqrt{dt}\cdot d_{m,n} \cdot \xi. 129 | 130 | Here, :math:`\xi` is a random vector of size :math:`(d)` distributed according to :math:`\mathcal{N}(0,1)`. 131 | 132 | Returns: 133 | numpy.ndarray: The generated noise. 134 | 135 | Note 136 | ---- 137 | The plain drift is used for the noise. Therefore, the noise vector is scaled with a different factor in each dimension, 138 | which motivates the name **anisotropic**. 139 | """ 140 | 141 | def __init__(self, 142 | norm: Callable = None, 143 | sampler: Callable = None): 144 | super().__init__(norm = norm, sampler = sampler) 145 | 146 | def __call__(self, dyn) -> ArrayLike: 147 | return np.sqrt(dyn.dt) * self.sample(dyn.drift) 148 | 149 | def sample(self, drift: ArrayLike) -> ArrayLike: 150 | r""" 151 | 152 | This function implements the anisotropic noise model. From the drift :math:`d = x - c(x)`, 153 | the noise vector is computed as 154 | 155 | .. math:: 156 | 157 | n_{m,n} = \sqrt{dt}\cdot d_{m,n} \cdot \xi. 158 | 159 | Here, :math:`\xi` is a random vector of size :math:`(d)` distributed according to :math:`\mathcal{N}(0,1)`. 160 | 161 | Returns: 162 | numpy.ndarray: The generated noise. 163 | 164 | Note 165 | ---- 166 | The plain drift is used for the noise. Therefore, the noise vector is scaled with a different factor in each dimension, 167 | which motivates the name **anisotropic**. 168 | """ 169 | 170 | return self.sampler(size = drift.shape) * drift 171 | 172 | class covariance_noise(noise): 173 | r""" 174 | 175 | This class implements the covariance noise model. Given the covariance matrix :math:`\text{Cov}(x)\in\mathbb{R}^{M\times d\times d}` of the ensemble, 176 | the noise vector is computed as 177 | 178 | .. math:: 179 | 180 | n_{m,n} = \sqrt{(1/\lambda)\cdot (1-\exp(-dt))^2} \cdot \sqrt{\text{Cov}(x)}\xi. 181 | 182 | Here, :math:`\xi` is a random vector of size :math:`(d)` distributed according to :math:`\mathcal{N}(0,1)`. 183 | """ 184 | 185 | def __init__(self, 186 | norm: Callable = None, 187 | sampler: Callable = None, 188 | mode = 'sampling'): 189 | super().__init__(norm = norm, sampler = sampler) 190 | self.mode = mode 191 | 192 | def __call__(self, dyn) -> ArrayLike: 193 | dyn.update_covariance() 194 | #factor = np.sqrt((1/dyn.lamda) * (1 - np.exp(-dyn.dt)**2))[(...,) + (None,) * (dyn.x.ndim - 2)] 195 | factor = np.sqrt((2*dyn.dt)/self.lamda(dyn))[(...,) + (None,) * (dyn.x.ndim - 2)] 196 | return factor * self.sample(dyn.drift, dyn.Cov_sqrt) 197 | 198 | def lamda(self, dyn): 199 | if self.mode == 'sampling': 200 | return 1/(1 + dyn.alpha) 201 | else: 202 | return 1 203 | 204 | def sample(self, drift:ArrayLike, Cov_sqrt:ArrayLike) -> ArrayLike: 205 | r""" 206 | This function implements the covariance noise model. 207 | Given the covariance matrix :math:`\text{Cov}(x)\in\mathbb{R}^{M\times d\times d}` of the ensemble, 208 | the noise vector is computed as 209 | 210 | .. math:: 211 | 212 | n_{m,n} = \sqrt{(1/\lambda)\cdot (1-\exp(-dt))^2} \cdot \sqrt{\text{Cov}(x)}\xi. 213 | 214 | Here, :math:`\xi` is a random vector of size :math:`(d)` distributed according to :math:`\mathcal{N}(0,1)`. 215 | 216 | Parameters 217 | ---------- 218 | drift (ArrayLike): The drift of the ensemble. 219 | Cov_sqrt (ArrayLike): The square root of the covariance matrix of the ensemble. 220 | 221 | Returns 222 | ------- 223 | ArrayLike: The covariance noise. 224 | 225 | """ 226 | 227 | z = self.sampler(size = drift.shape) 228 | return self.apply_cov_sqrt(Cov_sqrt, z) 229 | 230 | def apply_cov_sqrt(self, Cov_sqrt: ArrayLike, z:ArrayLike) -> ArrayLike: 231 | """ 232 | Applies the square root of the covariance matrix to the input tensor. 233 | 234 | Parameters 235 | ---------- 236 | Cov_sqrt (ArrayLike): The square root of the covariance matrix. 237 | z (ArrayLike): The input tensor of shape (batch_size, num_features, seq_length). 238 | 239 | Returns: 240 | ArrayLike: The output of the matrix-vector product. 241 | """ 242 | if Cov_sqrt.ndim == z.ndim: 243 | return np.einsum('kij,klj->kli', Cov_sqrt, z) 244 | elif Cov_sqrt.ndim == z.ndim + 1: 245 | return np.einsum('klij,klj->kli', Cov_sqrt, z) 246 | else: 247 | raise RuntimeError('Shape mismatch between Cov_sqrt and sampled vector!') 248 | 249 | def exponential_sampler(): 250 | """The function returns the exponential sampler.""" 251 | def _exponential_sampler(size=None): 252 | x = np.random.exponential(1.0, size) 253 | sign = np.random.choice([-1, 1], size) 254 | x *= sign 255 | return x 256 | return _exponential_sampler 257 | --------------------------------------------------------------------------------