diff --git a/README.md b/README.md index 43e7646b..c531e7ce 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,105 @@ tests/install/test_container_install mdmitry1/python311-dev Installation instructions for Ubuntu-22.04 can be followed using `homebrew` in place of `apt` +## Quickstart + +### Problem: find minimal distance between point (2,1) and unit circle
+ +

+Minimal Distance Problem

+ + Analytical solution for this problem:
+ ` +f(x*) = 6 - 2√5 ≈ 1.527864`, where `x* = (2/√5,1/√5) ≈ (0.894427, 0.447214)` +

+ Solution: see `bash` script [quickstart.sh](https://raw.githubusercontent.com/SMLP-Systems/smlp/smlp_quickstart/quickstart/quickstart.sh)

+ The script has 2 steps
+ - Step 1: Create input dataset and visualize the problem
+ - Step 2: Run SMLP
+ SMLP creates polynomial model and finds approximate solution
+ SMLP results + `f(x*) = 1.527865, x* = (0.894531, 0.447004)`
+ are within 0.05% accuracy for `f(x*)` and `x*` + +Running the script: +```bash +smlp_package_path=$(python3.11 -c 'import smlp; from os.path import dirname; print(dirname(smlp.__file__))') +$smlp_package_path/quickstart/quickstart.sh +``` +
+ +
+ Test case description
+ + **1.** *constraint_dora.json* - spec in json format
+ +``` +{ + "version": "1.2", + "variables": [ + {"label":"X1", "interface":"knob", "type":"real", "range":[-1.5,2.5], "rad-abs": 0.0}, + {"label":"X2", "interface":"knob", "type":"real", "range":[-1.5,2.0], "rad-abs": 0.0}, + {"label":"Y1", "interface":"output", "type":"real"} + ], + "alpha": "X1*X1+X2*X2<=1", + "objectives": {"objective1": "-Y1"} +} +``` + + Legend:
+ +``` + X1 - first controllable variable + X2 - second controlllable variable + Y1 - output function + rad-abs - sensitivity radius. + Zero radius means that solution sensitivity check is skipped + alpha - constraint depending on controllable variables + objective1 - optimization goal +``` + +
+ + **2.** SMLP command line arguments
+ + ``` + -data ${name}.csv.gz # input CSV dataset + -spec ${script_path}/${name_lc}.json # JSON spec file + -pref ${name} # output file prefix + -mode optimize # operation mode + -model poly_sklearn # model type + -epsilon 0.0000005 # convergence threshold +``` + +
+ +### Problem modification in the user area + +- Step 1: Copy the problem to the current directory and enter problem work area
+```bash +smlp_package_path=$(python3.11 -c 'import smlp; from os.path import dirname; print(dirname(smlp.__file__))') +\cp -rp $smlp_package_path/quickstart . +cd quickstart +``` +- Step 2: As an example, change constraint in order to get solution in rational numbers
+ Let's change circle radius to 2/√5, so squared radius will be 4/5
+ In order to do this, edit `constraint_dora.json` file and change right side of the inequality to be 4/5:
+ `"alpha": "X1*X1+X2*X2<=4/5",`

+ [Analytical solution](https://www.wolframalpha.com/input?i=Minimize%3A+f%28x1%2C+x2%29+%3D+%28x1+-+2%29%5E2+%2B+%28x2+-+1%29%5E2+subject+to+x1%5E2+%2B+x2%5E2+-+4%2F5+%3C%3D+0) for modified problem:
+ ` +f(x*) = 9/5 = 1.8`, where `x* = (4/5,2/5) = (0.8, 0.4)`

+- Step 3: Run the script from current directory +```bash +./quickstart.sh +``` +Expected SMLP results are within 0.03% accuracy for `f(x*)` and `x*`: +```bash +Working directory: /quickstart/Constraint_dora_results_ +X1 = 0.800048828125 +X2 = 0.3999021053314209 +Y1 = 1.8000002980730385 +``` + ## [Tutorial](https://github.com/SMLP-Systems/smlp/tree/master/tutorial) - Black-box optimization Eggholder Function diff --git a/misc/minimal_distance.png b/misc/minimal_distance.png new file mode 100644 index 00000000..68be010f Binary files /dev/null and b/misc/minimal_distance.png differ diff --git a/quickstart/constraint_dora.json b/quickstart/constraint_dora.json new file mode 100644 index 00000000..3b6f1472 --- /dev/null +++ b/quickstart/constraint_dora.json @@ -0,0 +1,12 @@ +{ + "version": "1.2", + "variables": [ + {"label":"X1", "interface":"knob", "type":"real", "range":[-1.5,2.5], "rad-abs": 0.0}, + {"label":"X2", "interface":"knob", "type":"real", "range":[-1.5,2.0], "rad-abs": 0.0}, + {"label":"Y1", "interface":"output", "type":"real"} + ], + "alpha": "X1*X1+X2*X2<=1", + "objectives": { + "objective1": "-Y1" + } +} diff --git a/quickstart/constraint_dora_dataset.py b/quickstart/constraint_dora_dataset.py new file mode 100755 index 00000000..7248036f --- /dev/null +++ b/quickstart/constraint_dora_dataset.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3.11 +""" +Constrained Optimization Example using SMLP +Classic Lagrange Multiplier Problem (appears in many optimization textbooks) + +Problem: + Minimize: f(x1, x2) = (x1 - 2)^2 + (x2 - 1)^2 + Subject to: x1^2 + x2^2 - 1 <= 0 (inside unit circle) + + Geometrically: Find the point on the unit circle closest to (2, 1) + +Analytical Solution (using Lagrange multipliers): + Setting ∇f = λ∇g where g(x1,x2) = x1² + x2² - 1: + - 2(x1-2) = 2λx1 → x1 = 2/(1+λ) + - 2(x2-1) = 2λx2 → x2 = 1/(1+λ) + - Substituting into constraint: 5 = (1+λ)² + - Solving: λ = √5 - 1 + +Expected Result: + Optimal point: x1 = 2/√5 ≈ 0.894427, x2 = 1/√5 ≈ 0.447214 + Minimum value: f = (√5 - 1)² ≈ 1.527864 + +Reference: Wolfram Alpha +https://www.wolframalpha.com/input?i=Minimize%3A+f%28x1%2C+x2%29+%3D+%28x1+-+2%29%5E2+%2B+%28x2+-+1%29%5E2+subject+to+x1%5E2+%2B+x2%5E2+-+1+%3C%3D+0 +""" + +from sys import argv +from numpy import linspace, meshgrid, cos, sin, pi, sqrt, inf +from pandas import read_csv, concat +from gzip import open as gzopen +from matplotlib import pyplot as plt + +def main(): +# Initial guess + rng = range(0, 1000) + x1_start, x1_stop = (-1.5, 2.5) + x2_start, x2_stop = (-1.5, 2.0) + x1 = linspace(x1_start, x1_stop, rng.stop) + x2 = linspace(x2_start, x2_stop, rng.stop) + X1, X2 = meshgrid(x1, x2) + Z = (X1 - 2)**2 + (X2 - 1)**2 + with gzopen('Constraint_dora.csv.gz',"wt") as ds: + ds.write("X1,X2,Y1\n") + [[ds.write(f"{X1[i][j]},{X2[i][j]},{Z[i][j]}\n") for j in rng] for i in rng] + + # Visualization + fig, ax = plt.subplots(figsize=(10, 8)) + + # Plot contours of objective function + contours = ax.contour(X1, X2, Z, levels=20, cmap='viridis', alpha=0.6) + ax.clabel(contours, inline=True, fontsize=8) + +# Plot constraint boundary (unit circle) + theta = linspace(0, 2*pi, 100) + circle_x = cos(theta) + circle_y = sin(theta) + ax.plot(circle_x, circle_y, 'r-', linewidth=2, label='Constraint boundary') + ax.fill(circle_x, circle_y, alpha=0.1, color='red', label='Feasible region') + + # Plot unconstrained optimum + ax.plot(2, 1, 'bs', markersize=12, label='Unconstrained optimum (2, 1)') + + ax.set_xlabel('x1', fontsize=12) + ax.set_ylabel('x2', fontsize=12) + ax.set_title('Constrained Optimization: Minimize f(x1,x2) subject to x1² + x2² ≤ 1', + fontsize=14) + # Plot constrained optimum + ax.plot(2/sqrt(5), 1/sqrt(5), 'go', markersize=12, label=f'Constrained optimum ({2/sqrt(5):.3f}, {1/sqrt(5):.3f})') + ax.legend(loc='upper right') + ax.grid(True, alpha=0.3) + ax.axis('equal') + ax.set_ylim(-1.5, 2.0) + + plt.tight_layout() + timeout = inf + if len(argv) > 2: + if '-timeout' == argv[1]: + timeout = int(argv[2]) + if not inf == timeout: + timer = fig.canvas.new_timer(interval=timeout*1000, callbacks=[(plt.close, [], {})]) + timer.start() + plt.show() + +if __name__ == "__main__": + main() diff --git a/quickstart/constraint_dora_poly_optimization_results_expected.txt b/quickstart/constraint_dora_poly_optimization_results_expected.txt new file mode 100644 index 00000000..17dc3ca3 --- /dev/null +++ b/quickstart/constraint_dora_poly_optimization_results_expected.txt @@ -0,0 +1,3 @@ +X1 = 0.89453125 +X2 = 0.4470043182373047 +Y1 = 1.5278653812779421 diff --git a/quickstart/quickstart.sh b/quickstart/quickstart.sh new file mode 100755 index 00000000..44778722 --- /dev/null +++ b/quickstart/quickstart.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +script_path="$(dirname "$(realpath "$0")")" +name=Constraint_dora +if [[ $# -gt 0 ]]; then + if [[ "-clean" == "$1" ]]; then + rm -rf ${name}_results_* 2>/dev/null + exit 0 + fi +fi +results_dir=${name}_results_$(date +%s) +rm -rf $results_dir 2>/dev/null +mkdir -p $results_dir +echo "Working directory: $(realpath $results_dir)" +cd $results_dir +log=${name}.log +dataset=${name}.csv.gz +name_lc="$(echo "$name" | tr '[:upper:]' '[:lower:]')" +"${script_path}/${name_lc}_dataset.py" #Create dataset and visualize the problem +results=${name}_poly_optimization_results.txt +rm -f "$results" 2>/dev/null +smlp_args=( + -data ${name}.csv.gz # input CSV dataset + -spec ${script_path}/${name_lc}.json # JSON spec file + -pref ${name} # output file prefix + -mode optimize # operation mode + -model poly_sklearn # model type + -epsilon 0.0000005 # convergence threshold +) + +smlp "${smlp_args[@]}" >"$log" 2>&1 +for var in X1 X2 Y1; do + echo "$var = $(jq ".${var}.value_in_config" ${name}_${name}_optimization_results.json)" 2>&1 | tee -a "$results" +done diff --git a/setup.py b/setup.py index c7bed053..6efee577 100644 --- a/setup.py +++ b/setup.py @@ -780,25 +780,29 @@ def run(self): shutil.copytree(str(installed_pkg), str(dest)) print(f"[smlp build] smlp extension copied to wheel at {dest}") - # 5. Copy Python source from src/smlp_py into smlp/smlp_py inside the wheel - smlp_py_src = REPO_ROOT / "src" / "smlp_py" - if smlp_py_src.is_dir(): - smlp_py_dest = dest / "smlp_py" # dest is already smlp/ - if smlp_py_dest.exists(): - shutil.rmtree(smlp_py_dest) - shutil.copytree(str(smlp_py_src), str(smlp_py_dest)) - print(f"[smlp build] smlp_py source copied to wheel at {smlp_py_dest}") - else: - print(f"[smlp build] WARNING: src/smlp_py not found at {smlp_py_src}, skipping.") - - # 6. Copy src/run_smlp.py into smlp/ inside the wheel - run_smlp_src = REPO_ROOT / "src" / "run_smlp.py" - if run_smlp_src.is_file(): - shutil.copy2(str(run_smlp_src), str(dest / "run_smlp.py")) - print(f"[smlp build] run_smlp.py copied to wheel at {dest / 'run_smlp.py'}") - else: - print(f"[smlp build] WARNING: src/run_smlp.py not found at {run_smlp_src}, skipping.") - + # 5. Copy Python sources from src/ into smlp/ inside the wheel + sources = [ + (REPO_ROOT / "src" / "smlp_py", dest / "smlp_py", "dir"), + (REPO_ROOT / "src" / "__init__.py", dest / "__init__.py", "file"), + (REPO_ROOT / "src" / "run_smlp.py", dest / "run_smlp.py", "file"), + (REPO_ROOT / "quickstart", dest / "quickstart", "dir"), + ] + for src, dst, kind in sources: + copy_success = False + if kind == "dir": + if src.is_dir(): + if dst.exists(): + shutil.rmtree(str(dst)) + shutil.copytree(str(src), str(dst)) + copy_success = True + else: + if src.is_file(): + shutil.copy2(str(src), str(dst)) + copy_success = True + if copy_success: + print(f"[smlp build] {src} copied to wheel at {dst}") + else: + print(f"[smlp build] WARNING: source is not found at {src}, skipping.") # --------------------------------------------------------------------------- # setup() diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..fe8594d4 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# This file is part of smlp.