Metadata-Version: 2.1
Name: oat-python-sdk
Version: 0.3.2
Summary: Python client for OAT (Optimization and Analysis Tooling) database
License: MIT
Keywords: optimization,constraint-solving,database
Author: Rikard Olsson
Requires-Python: >=3.10
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: pldag (>=0.10.17,<0.11.0)
Requires-Dist: requests (>=2.31.0,<3.0.0)
Description-Content-Type: text/markdown

# OatDB Python SDK

A Python client library for interacting with the OatDB (Optimization and Analysis Tooling) database backend.

## Features

- ✅ Full support for all 30 OatDB API functions
- ✅ Logical operations (AND, OR, XOR, NOT, IMPLY, EQUIV)
- ✅ Cardinality constraints (AtLeast, AtMost, Equal)
- ✅ Linear inequality constraints (GeLineq)
- ✅ Property management
- ✅ DAG operations (sub, sub_many, validate, ranks)
- ✅ Constraint propagation
- ✅ Optimization solver (solve, solve_many)
- ✅ Node deletion and management
- ✅ Alias support for named constraints
- ✅ Type hints for better IDE support

## Installation

```bash
pip install oat-python-sdk
```

Or with Poetry:

```bash
poetry add oat-python-sdk
```

## Quick Start

```python
from oatdb import OatClient

# Initialize client
client = OatClient("http://localhost:7061")

# Create primitives with bounds [min, max]
x = client.set_primitive("x", bound=1j)  # [0, 1]
y = client.set_primitive("y", bound=10j)  # [0, 10]

# Add properties
client.set_property(x, "name", "Variable X")

# Create constraints
constraint = client.set_and([x, y], alias="my_constraint")

# Extract DAG and solve
dag = client.sub(constraint)
solution = client.solve(
    dag=dag,
    objective=[
        {"id": "x", "coefficient": 1},
        {"id": "y", "coefficient": 2}
    ],
    assume=[
        # Force contraint to be true
        {"id": constraint, "bound": [1,1]}
    ],
    maximize=True
)

# Execute and get results
result = client.execute([solution.out])
print(result)
```

## Core Concepts

### Bounds

Bounds are represented as complex numbers where:
- Real part = lower bound
- Imaginary part = upper bound

```python
# Bound [0, 1]
bound = 1j

# Bound [5, 10]
bound = 5 + 10j

# Access bounds from solution
solution_data = result[solution.out]
x_bounds = solution_data["x"]  # [lower, upper] as list
```

### Function Calls and Buffering

All operations return `FunctionCall` objects that are buffered until you call `execute()`:

```python
# Buffer operations
x = client.set_primitive("x", bound=1j)
y = client.set_primitive("y", bound=1j)
constraint = client.set_and([x, y])

# Execute all buffered operations
result = client.execute()

# Or execute and get specific outputs
dag = client.sub(constraint)
result = client.execute([dag.out])
```

## Available Methods

### Primitive Operations
- `set_primitive(id: str, bound: complex = 1j, alias: Optional[str] = None)` - Create a single primitive
- `set_primitives(ids: List[str], bound: complex = 1j)` - Create multiple primitives
- `set_property(id: Union[str, FunctionCall], property: str, value: Any)` - Set node property

### Logical Operations
- `set_and(references: List, alias: Optional[str] = None)` - AND operation
- `set_or(references: List, alias: Optional[str] = None)` - OR operation
- `set_xor(references: List, alias: Optional[str] = None)` - XOR operation
- `set_not(references: List, alias: Optional[str] = None)` - NOT operation
- `set_imply(lhs, rhs, alias: Optional[str] = None)` - Implication (lhs → rhs)
- `set_equiv(lhs, rhs, alias: Optional[str] = None)` - Equivalence (lhs ↔ rhs)

### Cardinality Constraints
- `set_atleast(references: List, value: int, alias: Optional[str] = None)` - At least N must be true
- `set_atmost(references: List, value: int, alias: Optional[str] = None)` - At most N must be true
- `set_equal(references: List, value: Union[int, str], alias: Optional[str] = None)` - Exactly N must be true

### Linear Constraints
- `set_gelineq(coefficients: Dict[str, int], bias: int, alias: Optional[str] = None)` - Greater-or-equal linear inequality (ax + b >= 0)

### DAG Operations
- `sub(root)` - Extract sub-DAG from a root node
- `sub_many(roots: List)` - Extract multiple sub-DAGs
- `get_node_ids(dag)` - Get all node IDs in a DAG
- `validate(dag)` - Validate DAG structure
- `ranks(dag)` - Compute topological ranks

### Alias Operations
- `get_id_from_alias(alias: str)` - Get node ID from alias
- `get_alias(id)` - Get alias for a node ID
- `get_aliases_from_id(id)` - Get all aliases for a node ID
- `get_ids_from_aliases(aliases: List[str])` - Get IDs for multiple aliases

### Node Operations
- `get_node(id)` - Get a single node
- `get_nodes(ids: List)` - Get multiple nodes
- `get_property_values(property: str)` - Get all nodes with a specific property

### Propagation
- `propagate(assignments: List[Dict])` - Propagate constraints with assignments
- `propagate_many(many_assignments: List[List[Dict]])` - Propagate multiple assignment sets

### Solver
- `solve(dag, objective: List[Dict], assume: Optional[List[Dict]] = None, maximize: bool = True)` - Solve single optimization
- `solve_many(dag, objectives: List[List[Dict]], assume: Optional[List[Dict]] = None, maximize: bool = True)` - Solve multiple optimizations

### Deletion
- `delete_node(id)` - Delete a single node
- `delete_sub(roots: List)` - Delete sub-DAGs from roots

### Utility
- `health_check() -> bool` - Check server health
- `execute(outputs: Optional[List[str]] = None, clear_buffer: bool = True)` - Execute buffered operations
- `clear_buffer()` - Clear the operation buffer
- `get_buffer_size() -> int` - Get number of buffered operations
- `debug_payload() -> dict` - Get the JSON payload that would be sent

## Complete Example

```python
from oatdb import OatClient

# Initialize
client = OatClient("http://localhost:7061")

# Create primitives
x = client.set_primitive("x", bound=10j)
y = client.set_primitive("y", bound=10j)
z = client.set_primitive("z", bound=10j)

# Add metadata
client.set_property(x, "type", "variable")
client.set_property(x, "priority", 10)

# Create constraints
and_constraint = client.set_and([x, y], alias="both_xy")
or_constraint = client.set_or([y, z])
imply_constraint = client.set_imply(x, y)  # x → y

# Cardinality: at least 2 must be true
atleast_2 = client.set_atleast([x, y, z], 2)

# Linear constraint: 2x + 3y - z + 5 >= 0
linear = client.set_gelineq(
    coefficients={"x": 2, "y": 3, "z": -1},
    bias=5
)

# Combine all constraints
root = client.set_and([atleast_2, linear], alias="root")

# Extract DAG
dag = client.sub(root)

# Solve optimization: maximize 3x + 2y + z
solution = client.solve(
    dag=dag,
    objective=[
        {"id": "x", "coefficient": 3},
        {"id": "y", "coefficient": 2},
        {"id": "z", "coefficient": 1}
    ],
    assume=[
        {"id": root, "bound": [1,1]}
    ],
    maximize=True
)

# Execute and get results
result = client.execute([solution.out])

print("Solution:")
for var, bounds in result[solution.out].items():
    if isinstance(bounds, list) and len(bounds) == 2:
        print(f"  {var}: [{bounds[0]}, {bounds[1]}]")
```

## Working with Aliases

```python
from oatdb import OatClient

client = OatClient("http://localhost:7061")

# Create constraint with alias
x = client.set_primitive("x", bound=1j)
y = client.set_primitive("y", bound=1j)
constraint = client.set_and([x, y], alias="my_constraint")

# Execute creation
client.execute()

# Query by alias
id_from_alias = client.get_id_from_alias("my_constraint")
alias_from_id = client.get_alias(id_from_alias)

result = client.execute([id_from_alias.out, alias_from_id.out])
print(f"ID: {result[id_from_alias.out]}")
print(f"Alias: {result[alias_from_id.out]}")
```

## Propagation Example

```python
from oatdb import OatClient

client = OatClient("http://localhost:7061")

# Create AND constraint
x = client.set_primitive("x", bound=1j)
y = client.set_primitive("y", bound=1j)
and_gate = client.set_and([x, y], alias="and_gate")

# Execute creation
client.execute()

# Propagate: if AND is true, what can we infer?
prop_result = client.propagate(
    assignments=[
        {"id": "and_gate", "bound": [1, 1]}
    ]
)

result = client.execute([prop_result.out])
# Result will show that x and y must both be [1, 1]
print(result[prop_result.out])
```

## Testing

### Run the comprehensive test suite:

```bash
cd clients/Python
python tests/test_client.py
```

Or with Poetry:

```bash
poetry run python tests/test_client.py
```

**Note**: Make sure the OatDB server is running on `http://localhost:7061` before running tests.

### Run examples:

```bash
python examples.py
```

## Requirements

- Python >= 3.10
- requests >= 2.31.0

## Development

```bash
# Install with Poetry
poetry install

# Run tests
poetry run pytest

# Format code
poetry run black .

# Type checking
poetry run mypy oatdb
```

## License

MIT

## Links

- [GitHub Repository](https://github.com/yourusername/oat-db-rust-v2)
- [OatDB Documentation](https://github.com/yourusername/oat-db-rust-v2)
- [Rust Client](https://github.com/yourusername/oat-db-rust-v2/tree/main/clients/Rust)

