# AMN (Artificial Muscle Network)

[![CI](https://github.com/eldm-ethanmoore/artificial-muscle-network/actions/workflows/ci.yml/badge.svg)](https://github.com/eldm-ethanmoore/artificial-muscle-network/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/artificial-muscle-network.svg)](https://pypi.org/project/artificial-muscle-network/)
[![Downloads](https://img.shields.io/pypi/dm/artificial-muscle-network.svg)](https://pypi.org/project/artificial-muscle-network/)
[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-brightgreen.svg)]()

**A physics-inspired optimization library for scheduling, routing, and allocation problems.**

AMN treats optimization like a physical system: objectives create "forces" that push solutions toward better states, while constraints act as boundaries. The result is an intuitive, fast, and explainable optimizer that works out of the box.

```python
pip install artificial-muscle-network
```

---

## Why AMN?

### The Problem

You have a real-world optimization problem:
- **"How do I assign 50 tasks to 10 workers fairly?"**
- **"Which truck should deliver which packages?"**
- **"How should I allocate my budget across departments?"**

Your options today:

| Approach | Problem |
|----------|--------|
| **OR-Tools / CPLEX / Gurobi** | Steep learning curve, complex APIs, heavy dependencies, expensive licenses |
| **Genetic Algorithms** | Slow convergence, unpredictable results, lots of hyperparameters |
| **Greedy Heuristics** | Fast but often miss better solutions |
| **Write it yourself** | Time-consuming, error-prone |

### The AMN Solution

AMN gives you **scikit-learn simplicity** for operations research problems:

```python
from amn import Scheduler, Resource, Task, Constraint

scheduler = Scheduler(resources, tasks, constraints)
solution = scheduler.optimize()
```

That's it. No PhD required.

---

## Key Features

### 🚀 Zero Dependencies
Pure Python. No NumPy, no SciPy, no compiled extensions. Install it anywhere Python runs—your laptop, a Raspberry Pi, AWS Lambda, or a restricted corporate environment.

### 🎯 Multi-Objective by Design
Real problems have competing goals: minimize cost AND maximize fairness AND respect constraints. AMN handles this naturally by combining weighted objectives into a single energy function.

### 🔍 Explainable Results
Every solution comes with an energy trace showing how the optimizer converged. You can inspect the "forces" (gradients) to understand why the optimizer made specific choices.

### ⚡ Fast Convergence
Typically converges in 100-500 iterations. Most problems solve in under a second.

### 🧩 Extensible
Build custom objectives and constraints for your specific domain. The core optimizer is problem-agnostic.

---

## When Should You Use AMN?

### ✅ Great For

| Use Case | Example |
|----------|--------|
| **Staff Scheduling** | Assign nurses to shifts respecting skills and availability |
| **Task Assignment** | Distribute work orders to technicians fairly |
| **Delivery Routing** | Assign packages to drivers minimizing distance |
| **Resource Allocation** | Split budget across projects maximizing ROI |
| **Portfolio Optimization** | Balance investments across assets |
| **Load Balancing** | Distribute requests across servers evenly |
| **Classroom Scheduling** | Assign courses to rooms and time slots |
| **Crew Rostering** | Build airline crew schedules |

### ⚠️ Consider Alternatives For

| Use Case | Better Tool |
|----------|------------|
| **Exact optimal solutions required** | CPLEX, Gurobi (commercial MIP solvers) |
| **Millions of variables** | Specialized large-scale solvers |
| **Real-time (< 10ms) decisions** | Pre-computed lookup tables |
| **Traveling Salesman (exact)** | Concorde TSP solver |

### 🎯 Sweet Spot
AMN shines when you need a **good solution fast** for problems with:
- 10-1000 tasks/items
- 2-100 resources/vehicles/assets
- Multiple competing objectives
- Soft constraints that can be traded off

---

## What to Expect

### Performance

| Problem Size | Typical Time | Iterations |
|--------------|--------------|------------|
| 10 tasks, 3 workers | ~50ms | 100-200 |
| 50 tasks, 10 workers | ~200ms | 200-400 |
| 200 tasks, 20 workers | ~1s | 300-500 |
| 500 tasks, 50 workers | ~5s | 400-600 |

*Benchmarked on a 2020 MacBook Pro. Your mileage may vary.*

### Solution Quality

AMN finds **good solutions**, not necessarily **optimal** solutions. For most practical problems:
- Solutions are within 5-15% of optimal
- Trade-offs between objectives are well-balanced
- Constraints are respected (hard) or minimally violated (soft)

If you need provably optimal solutions, use a MIP solver. If you need a good solution in seconds with minimal setup, use AMN.

### Reproducibility

Set a `seed` for deterministic results:

```python
scheduler = Scheduler(resources, tasks, constraints, seed=42)
```

---

## Three Problem Domains

AMN provides high-level solvers for three common optimization domains.

### 1. 📅 Scheduling

**Problem:** Assign tasks to resources respecting skills, capacity, and fairness.

**Real-world examples:**
- Nurse shift scheduling
- Construction crew assignment
- Call center staffing
- Manufacturing job shop scheduling

```python
from amn import Scheduler, Resource, Task, Constraint

# Define your workforce
resources = [
    Resource("Alice", capacity=40, skills={"python", "sql"}),
    Resource("Bob", capacity=40, skills={"javascript", "sql"}),
    Resource("Carol", capacity=32, skills={"python", "javascript"}),  # Part-time
]

# Define the work
tasks = [
    Task("Backend API", duration=16, required_skills={"python"}),
    Task("Database Migration", duration=8, required_skills={"sql"}),
    Task("Frontend Update", duration=12, required_skills={"javascript"}),
    Task("Data Pipeline", duration=10, required_skills={"python", "sql"}),
]

# Define constraints
constraints = [
    Constraint.skill_match(),      # Workers must have required skills
    Constraint.capacity(max_hours=40),  # Respect weekly hours
    Constraint.no_overlap(),       # One worker per task
]

# Solve
scheduler = Scheduler(resources, tasks, constraints, seed=42)
solution = scheduler.optimize(max_iterations=300)

# Results
print(f"Solved in {solution.iterations} iterations")
print(f"Converged: {solution.converged}")
print(f"\nAssignments:")
for task, resource in solution.assignments:
    print(f"  {task.name} → {resource.name}")
```

**Output:**
```
Solved in 156 iterations
Converged: True

Assignments:
  Backend API → Alice
  Database Migration → Bob
  Frontend Update → Carol
  Data Pipeline → Alice
```

---

### 2. 🚚 Vehicle Routing

**Problem:** Assign deliveries to vehicles minimizing distance while respecting capacity.

**Real-world examples:**
- Last-mile delivery optimization
- Field service technician routing
- School bus routing
- Waste collection routes

```python
from amn import Router, Location, Vehicle, Delivery

# Define locations (x, y coordinates)
locations = [
    Location("Warehouse", 0, 0),      # Depot
    Location("Downtown", 2, 3),
    Location("Suburbs", 5, 1),
    Location("Mall", 3, 4),
    Location("Airport", 7, 2),
]

# Define fleet
vehicles = [
    Vehicle("Van 1", capacity=100, depot_index=0),
    Vehicle("Van 2", capacity=80, depot_index=0),
]

# Define deliveries
deliveries = [
    Delivery("Package A", location_index=1, demand=30),
    Delivery("Package B", location_index=2, demand=50),
    Delivery("Package C", location_index=3, demand=25),
    Delivery("Package D", location_index=4, demand=45),
]

# Solve
router = Router(
    locations, vehicles, deliveries,
    distance_weight=1.0,      # Minimize total distance
    fairness_weight=0.5,      # Balance load across vehicles
    seed=42
)
solution = router.optimize()

# Get organized routes
routes = router.get_routes(solution)
for vehicle_name, stops in routes.items():
    if stops:
        stop_names = [d.name for d in stops]
        total_demand = sum(d.demand for d in stops)
        print(f"{vehicle_name}: {stop_names} (load: {total_demand})")
```

**Output:**
```
Van 1: ['Package A', 'Package C'] (load: 55)
Van 2: ['Package B', 'Package D'] (load: 95)
```

---

### 3. 💰 Portfolio Allocation

**Problem:** Allocate budget across assets balancing risk and return.

**Real-world examples:**
- Investment portfolio optimization
- Marketing budget allocation
- R&D project funding
- Inventory stocking decisions

```python
from amn import Allocator, Asset

# Define investment options
assets = [
    Asset("US Stocks", expected_return=0.10, risk=0.18),
    Asset("Int'l Stocks", expected_return=0.08, risk=0.22),
    Asset("Bonds", expected_return=0.04, risk=0.05),
    Asset("Real Estate", expected_return=0.07, risk=0.12),
    Asset("Cash", expected_return=0.02, risk=0.01),
]

# Solve with different risk tolerances
for risk_tolerance in [0.2, 0.5, 0.8]:
    allocator = Allocator(
        assets,
        risk_tolerance=risk_tolerance,  # 0 = min risk, 1 = max return
        budget=1.0,
        seed=42
    )
    solution = allocator.optimize()
    weights = allocator.get_weights(solution)
    
    print(f"\nRisk Tolerance: {risk_tolerance}")
    print(f"Expected Return: {allocator.expected_return(solution):.1%}")
    print(f"Expected Risk: {allocator.expected_risk(solution):.1%}")
    print("Allocation:")
    for name, weight in sorted(weights.items(), key=lambda x: -x[1]):
        if weight > 0.01:
            print(f"  {name}: {weight:.1%}")
```

**Output:**
```
Risk Tolerance: 0.2
Expected Return: 3.8%
Expected Risk: 2.1%
Allocation:
  Cash: 45.2%
  Bonds: 38.1%
  Real Estate: 16.7%

Risk Tolerance: 0.5
Expected Return: 6.2%
Expected Risk: 8.4%
Allocation:
  Bonds: 31.2%
  Real Estate: 28.5%
  US Stocks: 24.1%
  Int'l Stocks: 16.2%

Risk Tolerance: 0.8
Expected Return: 8.1%
Expected Risk: 14.2%
Allocation:
  US Stocks: 42.3%
  Int'l Stocks: 28.7%
  Real Estate: 21.5%
  Bonds: 7.5%
```

---

## How It Works

AMN uses a physics-inspired approach called **gradient descent with momentum and constraint projection**.

### The Intuition

Imagine your solution as a ball on a hilly landscape:
- **Energy** = height on the landscape (lower is better)
- **Forces** = slopes pushing the ball downhill
- **Momentum** = the ball's velocity (helps escape local minima)
- **Damping** = friction (prevents oscillation)
- **Constraints** = walls that keep the ball in valid regions

### The Algorithm

```
1. Start with random assignment strengths (0-1 values)
2. Repeat until converged:
   a. Compute forces = -∇Energy - damping × velocity
   b. Update velocity += force × dt
   c. Update position += velocity × dt  
   d. Project position onto constraints (clip, normalize, etc.)
3. Decode continuous solution to discrete assignments
```

### Why "Artificial Muscle"?

The name comes from the analogy to biological muscles:
- Multiple "fibers" (objectives) pull in different directions
- The system finds equilibrium balancing all forces
- Constraints act like tendons limiting range of motion

---

## Advanced Usage

### Custom Objectives

```python
from amn import Optimizer
from amn.core.objectives import Objective

class MyObjective(Objective):
    def evaluate(self, y):
        # Return scalar energy (lower is better)
        return sum(v ** 2 for row in y for v in row)
    
    def gradient(self, y):
        # Return matrix of partial derivatives
        return [[2 * v for v in row] for row in y]

# Use with low-level optimizer
opt = Optimizer(
    variables=[[0.5, 0.5], [0.5, 0.5]],
    constraints=[],
    objectives=[MyObjective(weight=1.0)]
)
solution = opt.optimize()
```

### Tuning Parameters

```python
scheduler = Scheduler(
    resources, tasks, constraints,
    # Objective weights (higher = more important)
    coverage_weight=2.0,   # Assign all tasks
    fairness_weight=1.0,   # Balance workload
    sparsity_weight=0.5,   # Encourage 0/1 values
    
    # Optimizer dynamics
    dt=0.1,               # Time step (smaller = more stable)
    damping=0.1,          # Friction (higher = less oscillation)
    
    seed=42               # Reproducibility
)
```

### Inspecting Convergence

```python
solution = scheduler.optimize(max_iterations=500)

import matplotlib.pyplot as plt
plt.plot(solution.energies)
plt.xlabel('Iteration')
plt.ylabel('Energy')
plt.title('Convergence')
plt.show()
```

---

## FAQ

**Q: How does AMN compare to linear programming?**

Linear programming (LP) finds *provably optimal* solutions to problems with linear objectives and constraints. AMN finds *good* solutions to more general problems (non-linear objectives, soft constraints) much faster. Use LP when you need optimality guarantees; use AMN when you need speed and flexibility.

**Q: Can AMN handle hard constraints?**

Yes, through constraint projection. After each optimization step, solutions are projected back onto the feasible region. Some constraints (like capacity) are enforced in the decoder as well.

**Q: What if my problem doesn't fit the three domains?**

Use the low-level `Optimizer` class directly with custom objectives and constraints. The high-level classes (Scheduler, Router, Allocator) are convenience wrappers.

**Q: How do I choose objective weights?**

Start with equal weights, observe which objectives dominate, then adjust. Higher weight = that objective matters more. It's often useful to normalize objectives to similar scales.

**Q: Is AMN deterministic?**

Yes, if you set a `seed`. Without a seed, initialization is random.

---

## Installation

```bash
pip install artificial-muscle-network
```

Or install from source:

```bash
git clone https://github.com/eldm-ethanmoore/artificial-muscle-network.git
cd artificial-muscle-network
pip install -e .
```

Requires Python 3.9+. No dependencies.

---

## Documentation

- [Quickstart Guide](docs/quickstart.md)
- [API Reference](docs/api_reference.md)
- [How It Works](docs/how_it_works.md)
- [Tutorials](docs/tutorials/)
- [**Benchmarks vs OR-Tools, SciPy**](BENCHMARKS.md) ← Real performance data

---

## Contributing

Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT © AMN Contributors
