# ABM Framework - Detailed API Design

This document provides detailed specifications for each API in the ABM Framework, including method signatures, parameters, return types, and usage examples.

## Core Layer APIs

### Entity APIs

#### Agent API

**Purpose**: Base class for all entities in the simulation. Users subclass this to define custom agent types with specific behaviors and attributes.

**Base Class**: `Agent`

**Key Methods**:

- `__init__(self, **attributes)`: Initialize an agent with custom attributes
  - Parameters:
    - `**attributes`: Key-value pairs for agent properties (age, health_status, location, etc.)
  - Returns: None
  - Example: `agent = Person(age=25, health_status='healthy')`

**Usage Example**:
```python
class Person(Agent):
    def __init__(self, age, health_status):
        super().__init__(age=age, health_status=health_status)

# Agents don't have a step() method - they are event-driven
# Behavior is defined through events that are scheduled on the agent

class InfectionEvent(Event):
    def __init__(self, agent):
        self.agent = agent

    def execute(self):
        self.agent.set_attribute('status', 'infected')
        # Schedule recovery event
        recovery_event = RecoveryEvent(self.agent)
        self.agent._scheduler.schedule_event(recovery_event,
                                             self.agent._scheduler.get_current_time() + 10)

class RecoveryEvent(Event):
    def __init__(self, agent):
        self.agent = agent

    def execute(self):
        self.agent.set_attribute('status', 'recovered')
```

Note: Agents are event-driven rather than step-driven. They only perform work when an event scheduled for them is executed by the Scheduler. This is computationally efficient as inactive agents consume no CPU time.

#### Edge API

**Purpose**: Base class for undirected relationships between agents. Users subclass this to define domain-specific relationships.

**Base Class**: `Edge`

**Key Methods**:

- `__init__(self, node1, node2, **properties)`: Initialize an undirected edge between two agents
  - Parameters:
    - `node1`: First agent (order doesn't matter for undirected edges)
    - `node2`: Second agent
    - `**properties`: Key-value pairs for edge properties (strength, type, metadata, etc.)
  - Returns: None

- `get_property(key)`: Get the value of a specific edge property
  - Parameters:
    - `key`: Property name
  - Returns: Property value

- `set_property(key, value)`: Set an edge property
  - Parameters:
    - `key`: Property name
    - `value`: Property value
  - Returns: None

**Properties**:
- `node1`: First agent in the relationship
- `node2`: Second agent in the relationship
- `properties`: Dictionary of edge properties

**Usage Example**:
```python
class Friendship(Edge):
    def __init__(self, node1, node2, strength=1.0):
        super().__init__(node1, node2, strength=strength)

class ContactRelationship(Edge):
    def __init__(self, node1, node2, frequency=1):
        super().__init__(node1, node2, frequency=frequency)

class DirectedInfluence(DirectedEdge):
    """For directed relationships like influence or information flow"""
    def __init__(self, source, target, influence_strength=1.0):
        super().__init__(source, target, influence_strength=influence_strength)
```

#### DirectedEdge API

**Purpose**: Subclass of Edge for directed relationships where the order of nodes matters.

**Base Class**: `DirectedEdge(Edge)`

**Key Methods**:
- Inherits all methods from Edge

**Properties**:
- `source`: Source agent (node1 in the directed relationship)
- `target`: Target agent (node2 in the directed relationship)
- `node1`: Alias for source
- `node2`: Alias for target

**Usage Example**:
```python
class InformationFlow(DirectedEdge):
    def __init__(self, source, target, information_type='rumor'):
        super().__init__(source, target, information_type=information_type)

class Influence(DirectedEdge):
    def __init__(self, influencer, influenced, strength=0.5):
        super().__init__(influencer, influenced, strength=strength)
```

### Management APIs

#### Collective API

**Purpose**: Generic base class for managing collections of agents, providing query and filtering capabilities. Can be subclassed to create different types of collectives with specific membership criteria.

**Class**: `Collective`

**Key Methods**:

- `register_agent(agent)`: Register an agent with the collective
  - Parameters:
    - `agent`: Agent to register
  - Returns: None
  - Note: Can be overridden in subclasses to enforce membership criteria

- `unregister_agent(agent)`: Unregister an agent from the collective
  - Parameters:
    - `agent`: Agent to unregister
  - Returns: None

- `get_agents()`: Get all agents
  - Returns: List of all agents

- `get_agents_by_type(agent_type)`: Get agents of a specific type
  - Parameters:
    - `agent_type`: Agent class type
  - Returns: List of agents matching the type

- `filter_agents(predicate)`: Get agents matching a condition
  - Parameters:
    - `predicate`: Function that takes an agent and returns True if it matches
  - Returns: List of agents matching the predicate
  - Example: `collective.filter_agents(lambda a: a.attributes['age'] > 18)`

- `count()`: Get total number of agents
  - Returns: Integer count

- `count_by_type(agent_type)`: Get count of agents of a specific type
  - Parameters:
    - `agent_type`: Agent class type
  - Returns: Integer count

**Subclass Pattern**:

Collective can be subclassed to create specific types of agent groupings. The most common subclass is `Population`, which includes all agents. Other subclasses enforce specific membership criteria by overriding `register_agent()`:

```python
class Population(Collective):
    """Default collective that includes all agents"""
    pass

class AgeGroupCollective(Collective):
    """Maintains agents within a specific age range"""
    def __init__(self, min_age, max_age):
        super().__init__()
        self.min_age = min_age
        self.max_age = max_age

    def register_agent(self, agent):
        if self.min_age <= agent.attributes['age'] < self.max_age:
            super().register_agent(agent)
        # Agent is not registered if outside the age range

class HealthStatusCollective(Collective):
    """Maintains agents with a specific health status"""
    def __init__(self, health_status):
        super().__init__()
        self.health_status = health_status

    def register_agent(self, agent):
        if agent.attributes['health_status'] == self.health_status:
            super().register_agent(agent)

class HighRiskCollective(Collective):
    """Maintains high-risk agents (e.g., age > 65 and comorbidities)"""
    def register_agent(self, agent):
        age = agent.attributes.get('age', 0)
        comorbidities = agent.attributes.get('comorbidities', 0)
        if age > 65 and comorbidities > 0:
            super().register_agent(agent)
```

**Benefits of subclassing**:
- Automatic enforcement of membership criteria
- Clear, domain-specific collective definitions
- Composable: can use multiple collective instances simultaneously
- Maintains all base Collective methods (filtering, counting, iteration)
- Flexible: `Population` is just one example of a Collective

**Usage Example**:
```python
# During initialization:
all_agents = Population()  # Collective that includes all agents
age_18_65 = AgeGroupCollective(18, 65)
age_65_plus = AgeGroupCollective(65, 150)
healthy = HealthStatusCollective('healthy')

# Add agents (they are automatically placed in appropriate collectives)
for person in initial_agents:
    all_agents.add_agent(person)
    age_18_65.add_agent(person)
    age_65_plus.add_agent(person)
    healthy.add_agent(person)

# Query collectives during simulation:
adults = age_18_65.get_agents()
total_65_plus = age_65_plus.count()
healthy_count = healthy.count()

# Can still use filtering on collectives
high_income_elderly = age_65_plus.filter_agents(lambda a: a.attributes['income'] > 100000)
```

#### NetworkManager API

**Purpose**: Manages multiple graphs, one for each relationship type. Provides methods for creating and querying relationships.

**Class**: `NetworkManager`

**Key Methods**:

- `create_network(network_type, backend='networkx')`: Create a new network for a relationship type
  - Parameters:
    - `network_type`: String identifier for the network type (e.g., 'friendship', 'contact')
    - `backend`: Graph backend to use (default: 'networkx')
  - Returns: Network object

- `add_edge(network_type, source, target, edge)`: Add a relationship between two agents
  - Parameters:
    - `network_type`: Network type identifier
    - `source`: Source agent
    - `target`: Target agent
    - `edge`: Edge object representing the relationship
  - Returns: None

- `remove_edge(network_type, source, target)`: Remove a relationship between two agents
  - Parameters:
    - `network_type`: Network type identifier
    - `source`: Source agent
    - `target`: Target agent
  - Returns: None

- `get_neighbors(network_type, agent)`: Get all neighbors of an agent in a specific network
  - Parameters:
    - `network_type`: Network type identifier
    - `agent`: Agent to query
  - Returns: List of neighboring agents

- `get_edge(network_type, source, target)`: Get the edge object between two agents
  - Parameters:
    - `network_type`: Network type identifier
    - `source`: Source agent
    - `target`: Target agent
  - Returns: Edge object or None if no edge exists

- `get_network(network_type)`: Get the underlying network graph
  - Parameters:
    - `network_type`: Network type identifier
  - Returns: Graph object (NetworkX or other backend)

- `get_network_metrics(network_type, metric_type)`: Compute network metrics
  - Parameters:
    - `network_type`: Network type identifier
    - `metric_type`: Metric to compute (e.g., 'degree', 'clustering', 'centrality')
  - Returns: Dictionary of metric values

**Usage Example**:
```python
#During initialization:
network_manager = NetworkManager()
network_manager.create_network('friendship')
network_manager.create_network('contact')

#During simulation:
# Add relationships
friendship = Friendship(person1, person2, strength=0.8)
network_manager.add_edge('friendship', person1, person2, friendship)

# Query relationships
friends = network_manager.get_neighbors('friendship', person1)
friendship_edge = network_manager.get_edge('friendship', person1, person2)
```

#### Scheduler API

**Purpose**: Manages discrete time stepping and event scheduling.

**Class**: `Scheduler`

**Key Methods**:

- `schedule_event(event, time)`: Schedule an event to occur at a specific time
  - Parameters:
    - `event`: Event object to schedule
    - `time`: Time step at which the event should occur
  - Returns: None

- `advance_time()`: Advance to the next time step and execute pending events
  - Returns: Current time step

- `get_current_time()`: Get the current time step
  - Returns: Integer time step

- `get_pending_events(time)`: Get all events scheduled for a specific time
  - Parameters:
    - `time`: Time step to query
  - Returns: List of Event objects

**Usage Example**:
```python
#During initialization:
scheduler = Scheduler()

#During initialization and simulation:
# Schedule events
event1 = Event(agent, time=10, action=lambda: agent.recover())
scheduler.schedule_event(event1, 10)

#During simulation:
# Advance time
while scheduler.get_current_time() < 100:
    current_time = scheduler.advance_time()
    # Process any events that occurred at current_time
```

## Simulation Layer APIs

#### Parameters API

**Purpose**: Manages and provides access to simulation parameters in a structured, validated way. Parameter names (keys) are defined by the Workflow layer.

**Class**: `Parameters`

**Key Methods**:

- `__init__(self, parameter_dict)`: Initialize parameters from a dictionary
  - Parameters:
    - `parameter_dict`: Dictionary where keys are parameter names (defined by Workflow layer) and values are parameter values for this simulation
  - Returns: None

- `get(name, default=None)`: Get a parameter value by name
  - Parameters:
    - `name`: Parameter name (as defined by the Workflow layer)
    - `default`: Default value if parameter doesn't exist (optional)
  - Returns: Parameter value or default

- `set(name, value)`: Set a parameter value
  - Parameters:
    - `name`: Parameter name
    - `value`: Parameter value
  - Returns: None

- `validate(schema)`: Validate parameters against a schema
  - Parameters:
    - `schema`: Dictionary defining expected parameters and their types/constraints (typically provided by the Workflow layer)
  - Returns: Boolean indicating if parameters are valid
  - Raises: ValidationError if parameters don't match schema

- `to_dict()`: Convert parameters to a dictionary
  - Returns: Dictionary of all parameters

**Usage Example**:
```python
# Parameter names and structure are defined by the Workflow layer
# and passed down through Batch to Simulation
parameters = Parameters({
    'population_size': 1000,
    'infection_probability': 0.1,
    'contact_rate': 0.5,
    'max_steps': 100
})

# Access parameters
pop_size = parameters.get('population_size')
contact_rate = parameters.get('contact_rate', default=0.3)  # with default

# Validate against schema (schema defined by Workflow layer)
schema = {
    'population_size': {'type': int, 'min': 1},
    'infection_probability': {'type': float, 'min': 0, 'max': 1},
    'contact_rate': {'type': float, 'min': 0, 'max': 1},
    'max_steps': {'type': int, 'min': 1}
}

if parameters.validate(schema):
    print("Parameters are valid")

# Export parameters
config = parameters.to_dict()
```

**Design Note**: The set of parameter names is determined by the Workflow layer (ParameterSweep or Experiment). The Workflow layer generates parameter dictionaries, which are passed to Batch, which creates Simulation instances with Parameters objects initialized from those dictionaries.

#### WorldBuilder API

**Purpose**: Configures collectives, networks, and initial state for a simulation run based on parameters.

**Class**: `WorldBuilder`

**Key Methods**:

- `__init__(self, parameters)`: Initialize the builder with simulation parameters
  - Parameters:
    - `parameters`: Dictionary of parameters for the simulation
  - Returns: None

- `create_collective(agent_class, count, agent_attributes)`: Create initial collective of agents
  - Parameters:
    - `agent_class`: Agent class to instantiate
    - `count`: Number of agents to create (can be derived from parameters)
    - `agent_attributes`: Function or dictionary specifying attributes for each agent
  - Returns: Collective object (typically Population)

- `create_network(network_type, backend='networkx')`: Create a network for a relationship type
  - Parameters:
    - `network_type`: Network type identifier
    - `backend`: Graph backend to use
  - Returns: Network object

- `create_relationships(network_manager, collective, network_type, relationship_func)`: Create initial relationships
  - Parameters:
    - `network_manager`: NetworkManager to add edges to
    - `collective`: Collective of agents
    - `network_type`: Network type to initialize
    - `relationship_func`: Function that defines which agents to connect and how
  - Returns: None

- `build_simulation(simulation)`: Perform complete initialization of a simulation
  - Parameters:
    - `simulation`: Simulation object to initialize
  - Returns: Initialized Simulation object

**Usage Example**:
```python
parameters = {
    'population_size': 1000,
    'age_distribution': {'mean': 40, 'std': 20},
    'contact_network_density': 0.05,
}

world_builder = WorldBuilder(parameters)

# Create collective with random ages
def create_agents(count):
    return [
        Person(age=random.gauss(parameters['age_distribution']['mean'],
                               parameters['age_distribution']['std']))
        for _ in range(count)
    ]

collective = world_builder.create_collective(Person, parameters['population_size'], create_agents)

# Create networks
network_manager = NetworkManager()
network_manager.create_network('contact')

# Create relationships
def create_contacts(collective, network_manager):
    # Create random contacts based on density
    agents = collective.get_agents()
    for i in range(len(agents)):
        for j in range(i+1, len(agents)):
            if random.random() < parameters['contact_network_density']:
                edge = ContactEdge(agents[i], agents[j], frequency=1)
                network_manager.add_edge('contact', agents[i], agents[j], edge)

world_builder.create_relationships(network_manager, collective, 'contact', create_contacts)

# Or use the convenience method
simulation = Simulation(parameters)
world_builder.build_simulation(simulation)
```

#### SingleRun API

**Purpose**: Main orchestrator for a single simulation run. Coordinates all core components and manages the simulation lifecycle.

**Class**: `SingleRun`

**Key Methods**:

- `__init__(self, parameters)`: Initialize a simulation with a parameter set
  - Parameters:
    - `parameters`: Dictionary of parameters for this simulation
  - Returns: None

- `register_agent(agent)`: Register an agent with the simulation
  - Parameters:
    - `agent`: Agent to register
  - Returns: None

- `register_agents(agents)`: Register multiple agents with the simulation
  - Parameters:
    - `agents`: List of agents to register
  - Returns: None

- `create_relationship(network_type, source, target, edge)`: Create a relationship between two agents
  - Parameters:
    - `network_type`: Network type identifier
    - `source`: Source agent
    - `target`: Target agent
    - `edge`: Edge object
  - Returns: None

- `step()`: Execute one time step of the simulation
  - Returns: Current time step

- `run(steps)`: Run the simulation for a specified number of steps
  - Parameters:
    - `steps`: Number of steps to run
  - Returns: None

- `get_population()`: Get the population object
  - Returns: Population object

- `get_networks()`: Get the network manager
  - Returns: NetworkManager object

- `get_scheduler()`: Get the scheduler
  - Returns: Scheduler object

- `get_data(metric_name)`: Get collected data for a specific metric
  - Parameters:
    - `metric_name`: Name of the metric
  - Returns: Data collected during simulation

- `collect_metric(metric_name, collector_func)`: Register a custom metric collector
  - Parameters:
    - `metric_name`: Name for the metric
    - `collector_func`: Function that computes the metric
  - Returns: None

**Usage Example**:
```python
parameters = {
    'population_size': 1000,
    'contact_rate': 0.5,
    'infection_probability': 0.1
}

simulation = SingleRun(parameters)

# Add agents
for i in range(parameters['population_size']):
    person = Person(age=random.randint(18, 80))
    simulation.add_agent(person)

# Create relationships
# ... code to create relationships ...

# Run simulation
simulation.run(100)

# Access results
infected_over_time = simulation.get_data('infected_count')
```

## Batch Layer APIs

#### Batch API

**Purpose**: Executes multiple simulations in parallel with different parameter configurations.

**Class**: `Batch`

**Key Methods**:

- `__init__(self, parameter_configs)`: Initialize a batch with parameter configurations
  - Parameters:
    - `parameter_configs`: List of parameter dictionaries, one for each simulation
  - Returns: None

- `run(simulation_class, num_workers=1)`: Execute all simulations in parallel
  - Parameters:
    - `simulation_class`: Simulation class to instantiate
    - `num_workers`: Number of parallel workers (default: 1)
  - Returns: None

- `get_results()`: Get results from all simulations
  - Returns: List of result objects, one per simulation

- `get_result(index)`: Get results from a specific simulation
  - Parameters:
    - `index`: Index of the simulation
  - Returns: Result object

- `aggregate_results(metric_name, aggregation_func)`: Aggregate a metric across all simulations
  - Parameters:
    - `metric_name`: Name of the metric to aggregate
    - `aggregation_func`: Function to aggregate (e.g., np.mean, np.std)
  - Returns: Aggregated value

**Usage Example**:
```python
parameter_configs = [
    {'population_size': 1000, 'infection_prob': 0.1},
    {'population_size': 1000, 'infection_prob': 0.2},
    {'population_size': 1000, 'infection_prob': 0.3},
]

batch = Batch(parameter_configs)
batch.run(SingleRun, num_workers=4)

results = batch.get_results()
mean_infections = batch.aggregate_results('final_infected_count', np.mean)
```

## Workflow Layer APIs

#### ParameterSweep API

**Purpose**: Generates parameter configurations and runs a batch with those configurations.

**Class**: `ParameterSweep`

**Key Methods**:

- `__init__(self)`: Initialize a parameter sweep
  - Returns: None

- `add_parameter(name, values)`: Add a parameter with discrete values
  - Parameters:
    - `name`: Parameter name
    - `values`: List of values for this parameter
  - Returns: None

- `add_parameter_range(name, start, end, step)`: Add a parameter with a range of values
  - Parameters:
    - `name`: Parameter name
    - `start`: Start value
    - `end`: End value
    - `step`: Step size
  - Returns: None

- `generate_configurations(strategy='cartesian')`: Generate parameter configurations
  - Parameters:
    - `strategy`: Generation strategy ('cartesian' for full factorial, 'latin_hypercube' for sampling, etc.)
  - Returns: List of parameter configurations

- `run(simulation_class, num_workers=1)`: Generate configurations and run batch
  - Parameters:
    - `simulation_class`: Simulation class to instantiate
    - `num_workers`: Number of parallel workers
  - Returns: None

- `get_results()`: Get results from the batch
  - Returns: List of result objects

**Usage Example**:
```python
sweep = ParameterSweep()
sweep.add_parameter('infection_prob', [0.05, 0.1, 0.15, 0.2])
sweep.add_parameter_range('population_size', 100, 1000, 100)

configs = sweep.generate_configurations()
sweep.run(Simulation, num_workers=4)

results = sweep.get_results()
```

#### Experiment API

**Purpose**: Runs batches with calibrated parameters and compares multiple scenarios.

**Class**: `Experiment`

**Key Methods**:

- `__init__(self, calibrated_parameters)`: Initialize an experiment with calibrated parameters
  - Parameters:
    - `calibrated_parameters`: Set of parameter configurations from calibration
  - Returns: None

- `add_scenario(name, scenario_modifiers)`: Add a scenario/counterfactual to compare
  - Parameters:
    - `name`: Scenario name (e.g., 'baseline', 'intervention')
    - `scenario_modifiers`: Function that modifies parameters for this scenario
  - Returns: None

- `run(simulation_class, num_workers=1, random_seed=None)`: Run simulations across all scenarios
  - Parameters:
    - `simulation_class`: Simulation class to instantiate
    - `num_workers`: Number of parallel workers
    - `random_seed`: Seed for reproducibility and fair comparison
  - Returns: None

- `get_results(scenario_name=None)`: Get results for a specific scenario or all scenarios
  - Parameters:
    - `scenario_name`: Scenario name (optional; if None, returns all)
  - Returns: Dictionary of results by scenario

- `compare_scenarios(metric_name, statistical_test='t-test')`: Compare scenarios statistically
  - Parameters:
    - `metric_name`: Metric to compare
    - `statistical_test`: Type of test to use
  - Returns: Test results

**Usage Example**:
```python
calibrated_params = [
    {'infection_prob': 0.12, 'recovery_rate': 0.05},
]

experiment = Experiment(calibrated_params)
experiment.add_scenario('baseline', lambda p: p)  # No modification
experiment.add_scenario('intervention', lambda p: {**p, 'infection_prob': p['infection_prob'] * 0.5})

experiment.run(Simulation, num_workers=4, random_seed=42)

baseline_results = experiment.get_results('baseline')
intervention_results = experiment.get_results('intervention')
comparison = experiment.compare_scenarios('final_infected_count')
```

#### Calibrator API

**Purpose**: Performs statistical inference to estimate model parameters from data.

**Class**: `Calibrator`

**Key Methods**:

- `__init__(self, observed_data, model_outputs)`: Initialize the calibrator
  - Parameters:
    - `observed_data`: Observed data to fit against
    - `model_outputs`: Function that extracts relevant outputs from simulations
  - Returns: None

- `set_prior(parameter_name, prior_distribution)`: Set prior distribution for a parameter
  - Parameters:
    - `parameter_name`: Parameter name
    - `prior_distribution`: Prior distribution object
  - Returns: None

- `run(inference_method='abc', num_samples=1000)`: Run calibration
  - Parameters:
    - `inference_method`: Inference method ('abc', 'bayesian', 'emulator', etc.)
    - `num_samples`: Number of samples for inference
  - Returns: None

- `get_posterior_samples()`: Get posterior parameter samples
  - Returns: Array of posterior samples

- `get_posterior_mean()`: Get posterior mean parameter values
  - Returns: Dictionary of parameter means

- `sample_configurations(num_samples)`: Sample parameter configurations from posterior
  - Parameters:
    - `num_samples`: Number of configurations to sample
  - Returns: List of parameter configurations

**Usage Example**:
```python
observed_data = load_epidemiological_data()

def extract_model_outputs(simulation):
    return {'infected_count': simulation.get_data('infected_count')}

calibrator = Calibrator(observed_data, extract_model_outputs)
calibrator.set_prior('infection_prob', UniformPrior(0, 1))
calibrator.set_prior('recovery_rate', UniformPrior(0, 1))

calibrator.run(inference_method='abc', num_samples=1000)

posterior_samples = calibrator.get_posterior_samples()
posterior_mean = calibrator.get_posterior_mean()
calibrated_params = calibrator.sample_configurations(10)
```

#### Analysis API

**Purpose**: Post-simulation analysis, metrics computation, and visualization.

**Class**: `Analysis`

**Key Methods**:

- `__init__(self, results)`: Initialize analysis with simulation results
  - Parameters:
    - `results`: Results object from Batch or Experiment
  - Returns: None

- `compute_metric(metric_name, computation_func)`: Compute a custom metric
  - Parameters:
    - `metric_name`: Name for the metric
    - `computation_func`: Function that computes the metric
  - Returns: Metric values

- `compute_summary_statistics(metric_name)`: Compute summary statistics for a metric
  - Parameters:
    - `metric_name`: Name of the metric
  - Returns: Dictionary with mean, std, min, max, percentiles

- `statistical_test(metric_name, test_type='t-test')`: Perform statistical tests
  - Parameters:
    - `metric_name`: Metric to test
    - `test_type`: Type of test
  - Returns: Test results

- `plot_metric(metric_name, plot_type='line')`: Create visualization of a metric
  - Parameters:
    - `metric_name`: Metric to visualize
    - `plot_type`: Type of plot ('line', 'histogram', 'boxplot', etc.)
  - Returns: Plot object

- `export_results(format='csv')`: Export results to file
  - Parameters:
    - `format`: Export format ('csv', 'json', 'hdf5', etc.)
  - Returns: File path or data object

**Usage Example**:
```python
analysis = Analysis(batch.get_results())

# Compute metrics
summary = analysis.compute_summary_statistics('infected_count')
print(f"Mean infections: {summary['mean']}")

# Create visualizations
analysis.plot_metric('infected_count', plot_type='line')
analysis.plot_metric('infection_distribution', plot_type='histogram')

# Export results
analysis.export_results(format='csv')
```

#### Workflow API

**Purpose**: High-level orchestration of multi-step workflows.

**Class**: `Workflow`

**Key Methods**:

- `__init__(self)`: Initialize a workflow
  - Returns: None

- `add_step(name, step_func, inputs=None)`: Add a step to the workflow
  - Parameters:
    - `name`: Step name
    - `step_func`: Function that executes this step
    - `inputs`: Names of previous steps whose outputs feed into this step
  - Returns: None

- `run()`: Execute the complete workflow
  - Returns: Final results

- `get_step_results(step_name)`: Get results from a specific step
  - Parameters:
    - `step_name`: Name of the step
  - Returns: Step results

- `get_history()`: Get full workflow execution history
  - Returns: Execution history object

- `save(filepath)`: Save workflow and results
  - Parameters:
    - `filepath`: File path to save to
  - Returns: None

- `load(filepath)`: Load a saved workflow
  - Parameters:
    - `filepath`: File path to load from
  - Returns: Workflow object

**Usage Example**:
```python
workflow = Workflow()

def parameter_sweep():
    sweep = ParameterSweep()
    sweep.add_parameter('infection_prob', [0.1, 0.2, 0.3])
    sweep.run(Simulation)
    return sweep.get_results()

def calibrate(sweep_results):
    calibrator = Calibrator(observed_data, extract_outputs)
    calibrator.run(inference_method='abc')
    return calibrator.sample_configurations(10)

def experiment(calibrated_params):
    exp = Experiment(calibrated_params)
    exp.add_scenario('baseline', lambda p: p)
    exp.add_scenario('intervention', apply_intervention)
    exp.run(Simulation)
    return exp.get_results()

workflow.add_step('sweep', parameter_sweep)
workflow.add_step('calibration', calibrate, inputs=['sweep'])
workflow.add_step('experiment', experiment, inputs=['calibration'])

workflow.run()
sweep_results = workflow.get_step_results('sweep')
```
