Usage Guide

This guide demonstrates how to use the Zonotope package for various common tasks.

Creating Zonotopes

There are several ways to create a Zonotope:

Direct Initialization

import torch as t
from zonotope import Zonotope

# Create a zonotope with explicit tensors
center = t.tensor([1.0, 2.0])
infinity_terms = t.tensor([[0.1, 0.2], [0.3, 0.4]])
special_terms = t.tensor([[0.01, 0.02], [0.03, 0.04]])

z = Zonotope(
    center=center,
    infinity_terms=infinity_terms,
    special_terms=special_terms,
    special_norm=2
)

From Values

# Create from various types of inputs (lists, numpy arrays, etc.)
z = Zonotope.from_values(
    center=[1.0, 2.0],
    infinity_terms=[[0.1, 0.2], [0.3, 0.4]],
    special_terms=[[0.01, 0.02], [0.03, 0.04]],
    special_norm=2
)

From Bounds

# Create from lower and upper bounds
lower = t.tensor([0.0, 1.0])
upper = t.tensor([2.0, 3.0])

z = Zonotope.from_bounds(lower, upper, special_norm=2)

Basic Properties

Access various properties of a zonotope:

# Shape and dimensions
print(f"Shape: {z.shape}")
print(f"Number of variables: {z.N}")
print(f"Number of infinity error terms: {z.Ei}")
print(f"Number of special error terms: {z.Es}")
print(f"Total error terms: {z.E}")

# Device and dtype
print(f"Device: {z.device}")
print(f"Data Type: {z.dtype}")

# Norms
print(f"Special norm (p): {z.p}")
print(f"Dual norm (q): {z.q}")

Computing Concrete Bounds

To compute the concrete lower and upper bounds of a zonotope:

lower, upper = z.concretize()
print(f"Lower bounds: {lower}")
print(f"Upper bounds: {upper}")

Arithmetic Operations

Zonotopes support various arithmetic operations:

Addition

# Adding two zonotopes
z1 = Zonotope.from_values(center=[1.0, 2.0], infinity_terms=[[0.1], [0.2]])
z2 = Zonotope.from_values(center=[3.0, 4.0], infinity_terms=[[0.3], [0.4]])
z_sum = z1 + z2
print(f"Sum center: {z_sum.W_C}")

# Adding a scalar
z_shifted = z1 + 5.0
print(f"Shifted center: {z_shifted.W_C}")

Multiplication

# Scalar multiplication
z_scaled = z1 * 2.0
print(f"Scaled center: {z_scaled.W_C}")

# Right multiplication
z_scaled = 3.0 * z1
print(f"Scaled center: {z_scaled.W_C}")

# Tensor multiplication with einsum pattern
weights = t.tensor([[0.5, 0.5], [0.5, 0.5]])
pattern = "d, b d -> b"
z_weighted = z1.mul(weights, pattern)
print(f"Weighted center: {z_weighted.W_C}")

Sampling Points

Sample points from within the zonotope:

# Default sampling
samples = z.sample_point(n_samples=10)
print(f"Sampled points:\n{samples}")

# Binary sampling (corners of the zonotope)
corner_samples = z.sample_point(n_samples=10, use_binary_weights=True)
print(f"Corner samples:\n{corner_samples}")

# Sampling with only special or infinity terms
special_samples = z.sample_point(n_samples=5, include_infinity_terms=False)
infinity_samples = z.sample_point(n_samples=5, include_special_terms=False)

Tensor Operations

The zonotope package supports einops-style tensor operations:

Rearrangement

# Create a batched zonotope
batch_z = Zonotope.from_values(
    center=[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]],
    infinity_terms=[[[0.1, 0.2], [0.3, 0.4]], 
                   [[0.5, 0.6], [0.7, 0.8]], 
                   [[0.9, 1.0], [1.1, 1.2]]]
)

# Transpose dimensions
transposed_z = batch_z.rearrange("batch dim -> dim batch")
print(f"Original shape: {batch_z.shape}, Transposed shape: {transposed_z.shape}")

# Reshape
flattened_z = batch_z.rearrange("batch dim -> (batch dim)")
print(f"Flattened shape: {flattened_z.shape}")

Repetition

# Repeat a zonotope
repeated_z = z.repeat("dim -> repeat dim", repeat=3)
print(f"Original shape: {z.shape}, Repeated shape: {repeated_z.shape}")

Summation

# Sum along a dimension
summed_z = batch_z.sum(dim=0)
print(f"Original shape: {batch_z.shape}, Summed shape: {summed_z.shape}")

Device and Type Conversion

Convert zonotopes between devices and data types:

# Convert to float64
double_z = z.to(dtype=t.float64)
print(f"Original dtype: {z.dtype}, New dtype: {double_z.dtype}")

# Move to GPU (if available)
if t.cuda.is_available():
    gpu_z = z.to(device=t.device("cuda"))
    print(f"Original device: {z.device}, New device: {gpu_z.device}")

Slicing and Indexing

Zonotopes support various indexing and slicing operations:

# Create a batched zonotope
batch_z = Zonotope.from_values(
    center=[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
    infinity_terms=[[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]],
                   [[0.7, 0.8], [0.9, 1.0], [1.1, 1.2]]]
)

# Single index
first_item = batch_z[0]
print(f"First item shape: {first_item.shape}")

# Slice
first_two = batch_z[:2]
print(f"First two shape: {first_two.shape}")

# Multi-dimensional slice
subset = batch_z[0, :2]
print(f"Subset shape: {subset.shape}")

# Using ellipsis
last_dim = batch_z[..., 0]
print(f"Last dim shape: {last_dim.shape}")

Handling Error Terms

# Expand infinity error terms
z_expanded = z.clone()
original_ei = z_expanded.Ei
z_expanded.expand_infinity_error_terms(5)
print(f"Original terms: {original_ei}, Expanded terms: {z_expanded.Ei}")

# Ensure zeros are updated (normally handled automatically)
z.update_zeros()