Last active
December 29, 2025 20:41
-
-
Save mdmitry1/3b4985d4b7da9a8435c84d1a75f58e5f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "version": "1.2", | |
| "variables": [ | |
| {"label":"X1", "interface":"knob", "type":"real", "range":[0,5], "rad-abs": 0.0}, | |
| {"label":"X2", "interface":"knob", "type":"real", "range":[0,3], "rad-abs": 0.0}, | |
| {"label":"F1", "interface":"output", "type":"real"}, | |
| {"label":"F2", "interface":"output", "type":"real"} | |
| ], | |
| "alpha": "(X1-5)*(X1-5)+X2*X2-25 and -(X1-8)*(X1-8)-(X2+3)*(X2+3)+7.7", | |
| "objectives": { | |
| "objective1": "-F1", | |
| "objective1": "-F2" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/python3.12 | |
| """ | |
| Generic constraint function generator from JSON alpha attribute. | |
| Converts boolean constraints to numerical form for Pareto optimization. | |
| Handles OR operators using min() and AND operators using max(). | |
| """ | |
| import json | |
| import re | |
| from typing import List, Tuple | |
| from sys import argv | |
| from base64 import b64encode | |
| from hashlib import sha256 | |
| from os.path import realpath, basename | |
| def extract_variable_names(json_data: dict) -> List[str]: | |
| """ | |
| Extract variable names from the 'variables' section of JSON. | |
| Only includes variables with 'interface' == 'knob' (input variables). | |
| Args: | |
| json_data: Parsed JSON dictionary | |
| Returns: | |
| List of variable label names | |
| """ | |
| variables = json_data.get('variables', []) | |
| var_names = [] | |
| for var in variables: | |
| # Only include input variables (knobs), not outputs | |
| if var.get('interface') in ['knob', 'slider', 'input']: | |
| var_names.append(var.get('label')) | |
| return var_names | |
| def split_constraints_by_and(alpha_string: str) -> List[str]: | |
| """ | |
| Split alpha string into individual constraints using logical AND at the top level. | |
| Preserves OR operators and nested parentheses within constraints. | |
| Examples: | |
| "a and b and c" -> ["a", "b", "c"] | |
| "a and (b or c)" -> ["a", "(b or c)"] | |
| "p1==4 or (p1==8 and p2>3)" -> ["p1==4 or (p1==8 and p2>3)"] | |
| Args: | |
| alpha_string: The alpha constraint expression | |
| Returns: | |
| List of individual constraint strings | |
| """ | |
| constraints = [] | |
| current = [] | |
| paren_depth = 0 | |
| i = 0 | |
| while i < len(alpha_string): | |
| char = alpha_string[i] | |
| # Track parentheses depth | |
| if char == '(': | |
| paren_depth += 1 | |
| current.append(char) | |
| elif char == ')': | |
| paren_depth -= 1 | |
| current.append(char) | |
| # Only split on 'and' when at depth 0 (top level) | |
| elif paren_depth == 0 and alpha_string[i:i+5].lower() == ' and ': | |
| # Found a top-level AND | |
| constraint = ''.join(current).strip() | |
| if constraint: | |
| constraints.append(constraint) | |
| current = [] | |
| i += 4 # Skip past ' and' | |
| else: | |
| current.append(char) | |
| i += 1 | |
| # Add the last constraint | |
| constraint = ''.join(current).strip() | |
| if constraint: | |
| constraints.append(constraint) | |
| return constraints | |
| def convert_comparison_to_numerical(expr: str, operator: str, value: str) -> str: | |
| """ | |
| Convert a boolean comparison to numerical constraint form (≤ 0 for satisfied). | |
| Args: | |
| expr: Left-hand expression | |
| operator: Comparison operator (<=, >=, <, >, ==, !=) | |
| value: Right-hand value | |
| Returns: | |
| Numerical expression string | |
| """ | |
| expr = expr.strip() | |
| value = value.strip() | |
| if operator == '<=': | |
| # expr <= value → expr - value ≤ 0 | |
| return f"({expr}) - ({value})" | |
| elif operator == '>=': | |
| # expr >= value → value - expr ≤ 0 | |
| return f"({value}) - ({expr})" | |
| elif operator == '<': | |
| # expr < value → expr - value < 0 (treat as ≤ 0) | |
| return f"({expr}) - ({value})" | |
| elif operator == '>': | |
| # expr > value → value - expr < 0 (treat as ≤ 0) | |
| return f"({value}) - ({expr})" | |
| elif operator == '==': | |
| # expr == value → abs(expr - value) ≤ ε | |
| return f"abs(({expr}) - ({value})) - 1e-9" | |
| elif operator == '!=': | |
| # expr != value → -(abs(expr - value) - ε) ≤ 0 | |
| return f"-(abs(({expr}) - ({value})) - 1e-9)" | |
| else: | |
| raise ValueError(f"Unsupported operator: {operator}") | |
| def parse_simple_comparison(constraint: str) -> str: | |
| """ | |
| Parse a simple comparison and convert to numerical form. | |
| Args: | |
| constraint: Simple comparison like "x <= 5" or "x == 4" | |
| Returns: | |
| Numerical expression string | |
| """ | |
| # Try to match comparison operators (order matters - check multi-char first) | |
| operators = ['<=', '>=', '==', '!=', '<', '>'] | |
| for op in operators: | |
| if op in constraint: | |
| parts = constraint.split(op, 1) | |
| if len(parts) == 2: | |
| return convert_comparison_to_numerical(parts[0], op, parts[1]) | |
| # If no operator found, return as-is (might be a complex expression) | |
| return f"({constraint})" | |
| def split_by_or(expr: str) -> List[str]: | |
| """ | |
| Split expression by OR at the current parenthesis level. | |
| Args: | |
| expr: Expression that may contain OR operators | |
| Returns: | |
| List of OR clauses | |
| """ | |
| clauses = [] | |
| current = [] | |
| paren_depth = 0 | |
| i = 0 | |
| while i < len(expr): | |
| char = expr[i] | |
| if char == '(': | |
| paren_depth += 1 | |
| current.append(char) | |
| elif char == ')': | |
| paren_depth -= 1 | |
| current.append(char) | |
| elif paren_depth == 0 and expr[i:i+4].lower() == ' or ': | |
| # Found an OR at current level | |
| clause = ''.join(current).strip() | |
| if clause: | |
| clauses.append(clause) | |
| current = [] | |
| i += 3 # Skip past ' or' | |
| else: | |
| current.append(char) | |
| i += 1 | |
| # Add the last clause | |
| clause = ''.join(current).strip() | |
| if clause: | |
| clauses.append(clause) | |
| return clauses if len(clauses) > 1 else [expr] | |
| def split_by_and(expr: str) -> List[str]: | |
| """ | |
| Split expression by AND at the current parenthesis level. | |
| Args: | |
| expr: Expression that may contain AND operators | |
| Returns: | |
| List of AND clauses | |
| """ | |
| clauses = [] | |
| current = [] | |
| paren_depth = 0 | |
| i = 0 | |
| while i < len(expr): | |
| char = expr[i] | |
| if char == '(': | |
| paren_depth += 1 | |
| current.append(char) | |
| elif char == ')': | |
| paren_depth -= 1 | |
| current.append(char) | |
| elif paren_depth == 0 and expr[i:i+5].lower() == ' and ': | |
| # Found an AND at current level | |
| clause = ''.join(current).strip() | |
| if clause: | |
| clauses.append(clause) | |
| current = [] | |
| i += 4 # Skip past ' and' | |
| else: | |
| current.append(char) | |
| i += 1 | |
| # Add the last clause | |
| clause = ''.join(current).strip() | |
| if clause: | |
| clauses.append(clause) | |
| return clauses if len(clauses) > 1 else [expr] | |
| def convert_to_numerical(constraint: str) -> str: | |
| """ | |
| Recursively convert a boolean constraint expression to numerical form. | |
| Logic: | |
| - OR: min(clause1, clause2, ...) ≤ 0 means at least one is satisfied | |
| - AND: max(clause1, clause2, ...) ≤ 0 means all are satisfied | |
| - Comparison: convert to numerical form | |
| Args: | |
| constraint: Boolean constraint expression | |
| Returns: | |
| Numerical expression string | |
| """ | |
| constraint = constraint.strip() | |
| # Remove outer parentheses if present | |
| if constraint.startswith('(') and constraint.endswith(')'): | |
| # Check if these are matching outer parentheses | |
| depth = 0 | |
| for i, char in enumerate(constraint): | |
| if char == '(': | |
| depth += 1 | |
| elif char == ')': | |
| depth -= 1 | |
| if depth == 0 and i < len(constraint) - 1: | |
| break | |
| if i == len(constraint) - 1: | |
| constraint = constraint[1:-1].strip() | |
| # Check for OR at top level | |
| or_clauses = split_by_or(constraint) | |
| if len(or_clauses) > 1: | |
| # Convert each OR clause recursively | |
| numerical_clauses = [convert_to_numerical(clause) for clause in or_clauses] | |
| # Use min() - satisfied if ANY clause is satisfied | |
| return f"min({', '.join(numerical_clauses)})" | |
| # Check for AND at top level | |
| and_clauses = split_by_and(constraint) | |
| if len(and_clauses) > 1: | |
| # Convert each AND clause recursively | |
| numerical_clauses = [convert_to_numerical(clause) for clause in and_clauses] | |
| # Use max() - satisfied if ALL clauses are satisfied | |
| return f"max({', '.join(numerical_clauses)})" | |
| # Base case: simple comparison | |
| return parse_simple_comparison(constraint) | |
| def generate_constraint_function(constraint_num: int, constraint_expr: str, var_names: List[str]) -> str: | |
| """ | |
| Generate a Python constraint function that returns numerical values. | |
| Args: | |
| constraint_num: Constraint number (for function naming) | |
| constraint_expr: Constraint expression (boolean logic with comparisons) | |
| var_names: List of variable names | |
| Returns: | |
| Python function code as string | |
| """ | |
| # Generate function parameters | |
| params = ', '.join(var_names) | |
| # Convert to numerical form | |
| try: | |
| numerical_expr = convert_to_numerical(constraint_expr) | |
| # Generate function code | |
| func_code = f'''def constraint_C{constraint_num}({params}): | |
| """ | |
| C{constraint_num}: {constraint_expr} | |
| Numerical form (≤ 0 means satisfied): | |
| - OR clauses: min(...) - at least one must be satisfied | |
| - AND clauses: max(...) - all must be satisfied | |
| - Comparisons: converted to numerical expressions | |
| Returns: constraint value (≤ 0 means satisfied) | |
| """ | |
| return {numerical_expr}''' | |
| except Exception as e: | |
| # If conversion fails, return error function | |
| func_code = f'''def constraint_C{constraint_num}({params}): | |
| """ | |
| C{constraint_num}: {constraint_expr} | |
| ERROR: Could not convert to numerical form - {str(e)} | |
| """ | |
| raise NotImplementedError("Constraint conversion failed: {str(e)}")''' | |
| return func_code | |
| def generate_constraints_from_json(json_file: str) -> str: | |
| """ | |
| Read JSON file and generate Python constraint functions from alpha attribute. | |
| Args: | |
| json_file: Path to JSON file | |
| Returns: | |
| String containing all Python constraint function definitions | |
| """ | |
| # Read JSON file | |
| with open(json_file, 'r') as f: | |
| data = json.load(f) | |
| # Extract variable names | |
| var_names = extract_variable_names(data) | |
| if not var_names: | |
| raise ValueError("No input variables found in JSON file") | |
| # Get alpha constraint | |
| alpha = data.get('alpha', '') | |
| if not alpha: | |
| raise ValueError("No 'alpha' attribute found in JSON file") | |
| # Split into individual constraints by top-level AND | |
| constraints = split_constraints_by_and(alpha) | |
| # Generate header | |
| output = "# " + "=" * 76 + "\n" | |
| output += "# GENERATED CONSTRAINT FUNCTIONS FROM JSON ALPHA ATTRIBUTE\n" | |
| output += "# " + "=" * 76 + "\n" | |
| output += f"# Source: {basename(realpath(json_file))}\n" | |
| output += f"# Variables: {', '.join(var_names)}\n" | |
| output += f"# Alpha: {alpha}\n" | |
| output += f"# Number of constraints: {len(constraints)}\n" | |
| output += "#\n" | |
| output += "# Constraint Logic:\n" | |
| output += "# - OR: min(clause1, clause2) ≤ 0 (at least one satisfied)\n" | |
| output += "# - AND: max(clause1, clause2) ≤ 0 (all satisfied)\n" | |
| output += "# - Comparisons: converted to numerical form\n" | |
| output += "# " + "=" * 76 + "\n\n" | |
| # Generate each constraint function | |
| for idx, constraint in enumerate(constraints, 1): | |
| func_code = generate_constraint_function(idx, constraint, var_names) | |
| output += func_code + "\n\n" | |
| return output | |
| def main(rootpath: str = "./", json_file: str = "input.json", output_file: str = "constraints.py") -> str: | |
| print("=" * 80) | |
| print("GENERIC CONSTRAINT FUNCTION GENERATOR (NUMERICAL OUTPUT)") | |
| print("=" * 80) | |
| print() | |
| # Get JSON file from command line or use default | |
| try: | |
| # Generate constraint functions | |
| json_full_path = rootpath + json_file | |
| constraint_code = generate_constraints_from_json(json_full_path) | |
| # Print to console | |
| print(constraint_code) | |
| code_full_path = rootpath + output_file | |
| # Save to file | |
| with open(code_full_path, 'w') as f: | |
| f.write(constraint_code) | |
| print("=" * 80) | |
| print(f"✓ Constraints successfully generated and saved to '{code_full_path}'") | |
| print("=" * 80) | |
| with open(code_full_path, 'rb') as code_f: | |
| code = code_f.read() | |
| return sha256(b64encode(code)).hexdigest() | |
| except FileNotFoundError: | |
| print(f"ERROR: Could not find file '{json_full_path}'\n") | |
| return 1 | |
| except json.JSONDecodeError as e: | |
| print(f"ERROR: Invalid JSON in file '{json_full_path}'\n") | |
| print(f"Details: {e}") | |
| return 1 | |
| except Exception as e: | |
| print(f"ERROR: {e}\n") | |
| return 1 | |
| if __name__ == "__main__": | |
| rootpath = argv[1] if len(argv) > 1 else "." | |
| input_json = argv[2] if len(argv) > 2 else "input.json" | |
| output_code = argv[3] if len(argv) > 3 else "output_code.py" | |
| print(main(rootpath, input_json, output_code)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "version": "1.2", | |
| "variables": [ | |
| {"label":"P1", "interface":"knob", "type":"real", "range":[0,5], "rad-abs": 0.0}, | |
| {"label":"P2", "interface":"knob", "type":"real", "range":[0,3], "rad-abs": 0.0}, | |
| {"label":"F1", "interface":"output", "type":"real"}, | |
| {"label":"F2", "interface":"output", "type":"real"} | |
| ], | |
| "alpha": "P1==4 or (P1==8 and P2>3)", | |
| "objectives": { | |
| "objective1": "-F1", | |
| "objective1": "-F2" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| from or_constraints_ex import main | |
| from os import remove, popen, getenv | |
| from os.path import exists, dirname, realpath | |
| def test_or_constraints(monkeypatch, request): | |
| root_dir = str(request.config.rootpath) + '/' | |
| with monkeypatch.context() as m: | |
| test_path = dirname(realpath(root_dir + getenv('PYTEST_CURRENT_TEST').split(':')[0])) + "/" | |
| out = test_path + '/and_constraints.py' | |
| if exists(out): | |
| remove(out) | |
| out1 = test_path + '/or_constraints.py' | |
| if exists(out1): | |
| remove(out1) | |
| assert exists(out) == False | |
| m.setattr(sys, 'argv', ["or_constraints_ex"]) | |
| assert main(test_path,'bnh.json','and_constraints.py') == "17d012bbab8e46eb4a4d782c6e64da586f8bbc74496144313bad5f7d5cc9e19f" | |
| assert main(test_path,'or_ex.json','or_constraints.py') == "d35b2680b6ea28d205351578bb362c60afbd1972e1e32c5716f4902c24e29152" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment