# Select Solver

> Guides users through selecting the right PINA solver for their problem, or creating a custom solver when no built-in fits. Use when the user asks "what solver should I use", "how do I train this", "which solver", "choose a solver", "pick a solver", "solver selection", "how do I set up training", "what's the right solver", or similar. Also triggers when the user mentions specific solver names (PINN, CausalPINN, SelfAdaptivePINN, SupervisedSolver, etc.) or asks about custom training loops / custom solvers.

- Skill: `pina-org/select-solver` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pina-org/select-solver`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pina-org/select-solver/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: pina-org (https://skillmd.com/u/pina-org)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pina-org/select-solver

---


# Select a Solver for a PINA Problem

> [!IMPORTANT]
> Read [RULES.md](../RULES.md) before using this skill — it applies to all skills.

Use this skill to pick the correct PINA solver — or design a custom one — based
on your problem type, conditions, and training requirements.

PINA solvers are driven by **conditions** (data format) and **model count**
(single vs ensemble). The condition types in your `problem.conditions` dictate
which solver families are compatible; additional requirements (causality,
gradient enhancement, adaptive weighting) narrow the choice further.

## Step 1 — Understand the conditions

Start by identifying what kind of data your problem works with. Ask
conversationally if the user hasn't already built their problem object:

- **Time series?** — Your conditions use `n_windows` and `unroll_length`.
  The data has a sequential structure and the model rolls out predictions
  step by step.
- **Input → Target?** — You have input points and known target values.
  This is standard supervised / regression data: `Condition(input=..., target=...)`.
- **PDE equations + domains?** — Your conditions reference equations and
  domains/spatial points: `Condition(domain=..., equation=...)` or
  `Condition(input=..., equation=...)`.
- **Mixed?** — Many physics-informed problems mix equation conditions (for
  the PDE interior) with target conditions (for boundary data or
  observations).

You can inspect an existing problem object directly:

```python
for name, cond in problem.conditions.items():
    print(name, type(cond).__name__)
```

If the user hasn't defined conditions yet, ask if you can help setting up the
problem.

## Step 2 — Let the conditions drive the choice

Once the condition types are clear, map them to solver families. This is a
**decision tree**, not a menu — work through it with the user.

### Time series conditions

If every condition is (or includes) a `TimeSeriesCondition`:

```
→ AutoregressiveSingleModelSolver  (one model)
→ AutoregressiveEnsembleSolver     (multiple models, averaged)
```

No other solver family handles `TimeSeriesCondition`.

Key parameters:
- `eps` — noise injected during unrolling (regularisation)
- `unroll_length` — number of steps per forward pass

### Input → Target conditions only

If all conditions are `InputTargetCondition` (no equations, no domains):

```
→ SupervisedSingleModelSolver  (one model)
→ SupervisedEnsembleSolver     (N models with deep ensemble)
```

The `Ensemble` variant trains N independent copies and averages their
predictions at inference.

### Equation conditions (physics-informed)

If conditions include `InputEquationCondition` or `DomainEquationCondition`,
you're in the physics-informed family. Now ask two follow-up questions:

#### 1. How many models: one or multiple?

| Count | Options |
|-------|---------|
| Single model | `PhysicsInformedSingleModelSolver` (base) + its specialisations below |
| Multiple models (ensemble) | `PhysicsInformedEnsembleSolver` |

Multiple models trains N independent copies and averages predictions.

#### 2. Any special training requirement?

For the **single-model** physics-informed case, ask the user about these.
Each maps to a distinct solver:

| Requirement | Solver | What it does |
|---|---|---|
| Standard (no special needs) | `PhysicsInformedSingleModelSolver` | Plain PINN training |
| Gradient-enhanced | `GradientPhysicsInformedSingleModelSolver` | Adds gradient norm penalty — regularises the solution's derivatives. **Requires** `SpatialProblem` (needs `.spatial_variables`). |
| Causality in time | `CausalPhysicsInformedSingleModelSolver` | Applies causal temporal weighting so the solver learns forward in time. **Requires** `TimeDependentProblem`. |
| Per-point residual attention | `RBAPhysicsInformedSingleModelSolver` | Re-weights collocation points by residual magnitude across epochs (focus on hard regions). |
| Per-parameter adaptive weights | `SelfAdaptivePhysicsInformedSolver` | Learns a per-point weight through a second model (min-max optimisation). |
| Adversarial discriminator | `CompetitivePhysicsInformedSolver` | A discriminator model bets on point residuals; solver must fool it — minimax game. |

**Self-adaptive and competitive are not single-model solvers** — they manage
multiple optimisers internally. `SelfAdaptive` trains a weight network
alongside the model; `Competitive` trains a discriminator.

**Ensemble** + special requirements? The `PhysicsInformedEnsembleSolver`
does not layer gradient/causal/self-adaptive on top. For those combinations,
create a custom solver (Step 4).

## Step 3 — Assemble the solver

Once the solver is chosen, the construction pattern is consistent:

```python
from pina.solver import PhysicsInformedSingleModelSolver
# or any other solver

solver = PhysicsInformedSingleModelSolver(
    problem=problem,
    model=model,
    learning_rate=0.001,
    # optional:
    # loss=torch.nn.MSELoss(),
    # weighting=my_weighting,
    # scheduler=torch.optim.lr_scheduler.CosineAnnealingLR(...),
    # batch_size=32,
)
```

## Step 4 — Creating a custom solver

When no built-in solver matches your requirements, PINA's architecture makes
it straightforward to compose one. The key building blocks are **mixins** —
reusable `training_step` and `_compute_condition_loss` overrides.

### When to create a custom solver

- You need a combination PINA doesn't provide (e.g., causal weighting +
  ensemble, or gradient-enhanced + competitive)
- Your loss computation has custom per-condition logic not covered by the
  existing `_compute_condition_loss` overrides
- You need a multi-model setup that isn't just an ensemble (e.g., two models
  exchanging information in a non-adversarial way)
- You need to override `training_step` for a non-standard optimiser loop

### How PINA solvers are composed

Every solver is a class hierarchy:

```
BaseSolver
 ├── training/val/test_step
 ├── _compute_condition_loss
 ├── _prepare_condition_data
 ├── _regularize_condition_loss
 └── _loss_from_residual

SingleModelSolver(BaseSolver)
 └── automatic_optimization=True, single forward

MultiModelSolver(BaseSolver)
 └── automatic_optimization=False, multiple optimisers

EnsembleSolver(BaseSolver)          — uses MultiModelSolver internally
 └── automatic_optimization=False, averages N models
```

Mixins override specific methods:

| Mixin | Overrides | Purpose |
|---|---|---|
| `PhysicsInformedMixin` | `validation_step`, `test_step` | Enables autograd in no-grad contexts |
| `GradientEnhancedMixin` | `_prepare_condition_data`, `_regularize_condition_loss` | Gradient penalty regularisation |
| `ResidualBasedAttentionMixin` | `_regularize_condition_loss` | Per-point attention weights |
| `AutoregressiveMixin` | `_loss_from_residual` | Step-wise adaptive loss |
| `ManualOptimizationMixin` | `training_step` | Disables automatic optimisation |
| `ConditionAggregatorMixin` | `training_step` | Iterates conditions, aggregates losses |

### Custom solver recipe

1. **Pick a base**: `SingleModelSolver` (auto-optim) or `MultiModelSolver`
   (manual optim for multiple optimisers) or `EnsembleSolver` (N-model avg).

2. **Mix in behaviours** by inheriting the mixins in the right order (mixins
   first so their method overrides take priority):

   ```python
   from pina.solver import SingleModelSolver
   from pina.solver.mixin import GradientEnhancedMixin, PhysicsInformedMixin

   class MyCustomSolver(
       PhysicsInformedMixin,      # 1st — ensures autograd in val/test
       GradientEnhancedMixin,     # 2nd — adds gradient penalty
       SingleModelSolver,         # 3rd — base training loop
   ):
       def __init__(self, problem, model, **kwargs):
           super().__init__(problem=problem, model=model, **kwargs)
   ```

3. **Override `_compute_condition_loss`** if you need custom per-condition
   logic (e.g., different loss functions per condition, special weighting).
   The signature is:

   ```python
   def _compute_condition_loss(self, condition, data, batch_idx):
       # data is a dict like {"input": tensor, "target": tensor}
       # condition is the Condition object
       ...
       return scalar_tensor
   ```

4. **Override `training_step`** only for fundamental changes to the optimiser
   loop (e.g., alternating min-max, multi-stage schedules):

   ```python
   def training_step(self, batch, batch_idx):
       # Custom loop: zero_grad → compute loss → manual_backward → step
       ...
   ```

### Example: Causal ensemble

```python
from pina.solver import EnsembleSolver

class CausalEnsemblePINN(EnsembleSolver):
    """Ensemble of models with causal temporal weighting."""
    def _compute_condition_loss(self, condition, data, batch_idx):
        # Inject causal weighting logic here
        # (iterate over time segments, apply causal mask)
        ...
```

When guiding a user through creating a custom solver, discuss their specific
need, identify which existing mixin or base class covers most of it, and then
describe only the method they need to override. Avoid generating the full
solver class unless the user explicitly asks for it.

## Templates

### Choosing from conditions

Use this pattern when the problem object already exists:

```python
from pina.solver import (
    SupervisedSingleModelSolver,
    PhysicsInformedSingleModelSolver,
    AutoregressiveSingleModelSolver,
    PhysicsInformedEnsembleSolver,
)

condition_types = {type(c).__name__ for _, c in problem.conditions.items()}

if "TimeSeriesCondition" in condition_types:
    solver = AutoregressiveSingleModelSolver(problem=problem, model=model)
elif "InputTargetCondition" in condition_types and not \
     ("InputEquationCondition" in condition_types or
      "DomainEquationCondition" in condition_types):
    solver = SupervisedSingleModelSolver(problem=problem, model=model)
else:
    solver = PhysicsInformedSingleModelSolver(problem=problem, model=model)
```

### Custom solver stub

```python
from pina.solver import SingleModelSolver

class MySolver(SingleModelSolver):
    """Custom solver for specialised loss logic."""

    def _compute_condition_loss(self, condition, data, batch_idx):
        # Your custom loss computation
        # data keys: "input", "target" (if InputTargetCondition)
        #            "input", "equation" (if InputEquationCondition)
        ...
        return loss

    def training_step(self, batch, batch_idx):
        # Only if you need a custom optimiser loop
        ...
```

## Checklist

- [ ] Condition types identified (time series / supervised / physics-informed / mixed)
- [ ] Condition-to-solver-family mapping discussed with the user
- [ ] Single-model vs ensemble decision made
- [ ] Special training requirements assessed (causality, gradient, adaptive, competitive)
- [ ] If custom solver: mixin composition pattern understood, only necessary overrides discussed
- [ ] Solver constructed with correct signature (problem, model/models, extra params)

