| name | select-solver |
| description | 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. |
| license | MIT |
| compatibility | opencode, codex, claude |
| metadata | {"audience":"users","workflow":"solver-selection"} |
Select a Solver for a PINA Problem
[!IMPORTANT]
Read 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:
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:
from pina.solver import PhysicsInformedSingleModelSolver
solver = PhysicsInformedSingleModelSolver(
problem=problem,
model=model,
learning_rate=0.001,
)
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
-
Pick a base: SingleModelSolver (auto-optim) or MultiModelSolver
(manual optim for multiple optimisers) or EnsembleSolver (N-model avg).
-
Mix in behaviours by inheriting the mixins in the right order (mixins
first so their method overrides take priority):
from pina.solver import SingleModelSolver
from pina.solver.mixin import GradientEnhancedMixin, PhysicsInformedMixin
class MyCustomSolver(
PhysicsInformedMixin,
GradientEnhancedMixin,
SingleModelSolver,
):
def __init__(self, problem, model, **kwargs):
super().__init__(problem=problem, model=model, **kwargs)
-
Override _compute_condition_loss if you need custom per-condition
logic (e.g., different loss functions per condition, special weighting).
The signature is:
def _compute_condition_loss(self, condition, data, batch_idx):
...
return scalar_tensor
-
Override training_step only for fundamental changes to the optimiser
loop (e.g., alternating min-max, multi-stage schedules):
def training_step(self, batch, batch_idx):
...
Example: Causal ensemble
from pina.solver import EnsembleSolver
class CausalEnsemblePINN(EnsembleSolver):
"""Ensemble of models with causal temporal weighting."""
def _compute_condition_loss(self, condition, data, batch_idx):
...
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:
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
from pina.solver import SingleModelSolver
class MySolver(SingleModelSolver):
"""Custom solver for specialised loss logic."""
def _compute_condition_loss(self, condition, data, batch_idx):
...
return loss
def training_step(self, batch, batch_idx):
...
Checklist