Skip to main content

Artificial Muscle Network: physics-inspired optimization for scheduling, routing, and allocation

Project description

AMN (Artificial Muscle Network)

CI PyPI Downloads Python 3.9+ License: MIT Zero Dependencies

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.

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:

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:

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
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
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
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

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

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

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

pip install artificial-muscle-network

Or install from source:

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


Contributing

Contributions welcome! See CONTRIBUTING.md.

License

MIT © AMN Contributors

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

artificial_muscle_network-2.0.0.tar.gz (29.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

artificial_muscle_network-2.0.0-py3-none-any.whl (25.5 kB view details)

Uploaded Python 3

File details

Details for the file artificial_muscle_network-2.0.0.tar.gz.

File metadata

File hashes

Hashes for artificial_muscle_network-2.0.0.tar.gz
Algorithm Hash digest
SHA256 9114e77d1cbc3bae9aae7d4749f32dacbc864fe9b2c2ae22fda9d4458a635442
MD5 bd6eba4b084a09b26669aef3f9698586
BLAKE2b-256 e5d1c60f04233f13685077f35e8cde685b7302b4a83968d9b614bc704a14cc26

See more details on using hashes here.

File details

Details for the file artificial_muscle_network-2.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for artificial_muscle_network-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 163f9143fa6860f1583c6dca29ee9b6fdbc8417ba7f9cf1ee56d8deafc52639c
MD5 7e45bc64f4f6b3a41ffe6f2fd90a2b41
BLAKE2b-256 020a9864bb9ab15e10bb814244a72d66b14b1ba9d47ab9658f80f14a6b4e40cd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page