Skip to content

Instantly share code, notes, and snippets.

@mdmitry1
Last active December 29, 2025 20:41
Show Gist options
  • Select an option

  • Save mdmitry1/3b4985d4b7da9a8435c84d1a75f58e5f to your computer and use it in GitHub Desktop.

Select an option

Save mdmitry1/3b4985d4b7da9a8435c84d1a75f58e5f to your computer and use it in GitHub Desktop.
{
"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"
}
}
#!/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))
{
"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"
}
}
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