Introduction
Implicit disciplines define residuals R(x,y) that must be solved to find outputs. They are suitable for analyses involving systems of equations, iterative solvers, or problems where outputs cannot be computed directly.
Understanding Implicit Formulations
An implicit discipline defines a residual function:
The discipline must:
- Compute residuals: Evaluate R for given inputs and outputs
- Solve residuals: Find outputs such that R = 0
- Compute gradients (optional): Provide ∂R/∂inputs and ∂R/∂outputs
Simple Implicit Discipline
Here's a basic example solving x² - y = 0:
#include <cmath>
private:
}
}
double x = inputs.at("x")(0);
double y = outputs.at("y")(0);
residuals.at("y")(0) = std::pow(x, 2) - y;
}
double x = inputs.at("x")(0);
outputs.at("y")(0) = std::pow(x, 2);
}
double x = inputs.at("x")(0);
partials[{"y", "x"}](0) = 2.0 * x;
partials[{"y", "y"}](0) = -1.0;
}
};
void AddInput(const std::string &name, const std::vector< int64_t > &shape, const std::string &units)
Declares an input.
virtual void SetupPartials()
Setup function that is called by the server when the client calls the setup RPC.
void AddOutput(const std::string &name, const std::vector< int64_t > &shape, const std::string &units)
Declares an output.
virtual void Setup()
Setup function that is called by the server when the client calls the setup RPC.
Implicit discipline class.
Definition implicit.h:291
void DeclarePartials(const std::string &f, const std::string &x)
Declare a (set of) partial(s) for the discipline.
virtual void SolveResiduals(const philote::Variables &inputs, philote::Variables &outputs)
Solves the residuals to obtain the outputs for the discipline.
virtual void ComputeResidualGradients(const philote::Variables &inputs, const philote::Variables &outputs, Partials &partials)
Computes the gradients of the residuals evaluation for the discipline.
virtual void ComputeResiduals(const philote::Variables &inputs, const philote::Variables &outputs, philote::Variables &residuals)
Computes the residual for the discipline.
std::map< std::string, philote::Variable > Variables
Definition variable.h:404
std::map< std::pair< std::string, std::string >, philote::Variable > Partials
Definition variable.h:405
Quadratic Equation Solver
A more complex example solving ax² + bx + c = 0:
#include <cmath>
private:
void Setup() override {
}
void SetupPartials() override {
}
double a = inputs.at("a")(0);
double b = inputs.at("b")(0);
double c = inputs.at("c")(0);
double x = outputs.at("x")(0);
residuals.at("x")(0) = a * std::pow(x, 2) + b * x + c;
}
double a = inputs.at("a")(0);
double b = inputs.at("b")(0);
double c = inputs.at("c")(0);
outputs.at("x")(0) = (-b + std::sqrt(std::pow(b, 2) - 4*a*c)) / (2*a);
}
double a = inputs.at("a")(0);
double b = inputs.at("b")(0);
double x = outputs.at("x")(0);
partials[{"x", "a"}](0) = std::pow(x, 2);
partials[{"x", "b"}](0) = x;
partials[{"x", "c"}](0) = 1.0;
partials[{"x", "x"}](0) = 2*a*x + b;
}
};
Definition quadratic_server.cpp:54
Starting an Implicit Server
Server setup is identical to explicit disciplines:
#include <grpcpp/grpcpp.h>
std::string address("localhost:50051");
grpc::ServerBuilder builder;
builder.AddListeningPort(address, grpc::InsecureServerCredentials());
std::unique_ptr<grpc::Server> server = builder.BuildAndStart();
std::cout << "Server listening on " << address << std::endl;
server->Wait();
return 0;
}
void RegisterServices(grpc::ServerBuilder &builder)
Registers all services with a gRPC channel.
int main()
Definition paraboloid_client.cpp:50
Required Methods
ComputeResiduals()
Must be implemented - evaluates the residual:
double x = inputs.at("x")(0);
double y = outputs.at("y")(0);
residuals.at("y")(0) = f(x, y);
}
SolveResiduals()
Must be implemented - solves for outputs:
double x = inputs.at("x")(0);
outputs.at("y")(0) = solve(x);
}
ComputeResidualGradients()
Optional - computes Jacobian of residuals:
partials[{"output", "input"}](0) = dR_dx;
partials[{"output", "output"}](0) = dR_dy;
}
Key Differences from Explicit Disciplines
| Aspect | Explicit | Implicit |
| Function signature | Compute(inputs, outputs) | ComputeResiduals(inputs, outputs, residuals) |
| What it computes | Outputs directly | Residuals R(inputs, outputs) |
| Additional method | - | SolveResiduals(inputs, outputs) |
| Gradients | ∂outputs/∂inputs | ∂R/∂inputs and ∂R/∂outputs |
| Use case | Direct calculations | Systems of equations |
Lifecycle
The discipline lifecycle:
- Initialize() - Define available options
- SetOptions() - Client sets option values
- Configure() - Process option values
- Setup() - Define variables
- SetupPartials() - Declare available gradients
- ComputeResiduals() - Called to evaluate residuals
- SolveResiduals() - Called to solve for outputs
- ComputeResidualGradients() - Called for gradient evaluation
Best Practices
- Implement both required methods: ComputeResiduals() and SolveResiduals()
- Include output gradients: Declare partials w.r.t. both inputs and outputs
- Test residual accuracy: After solving, R should be near zero
- Handle solver failures: Check for convergence in SolveResiduals()
- Use const appropriately: Inputs and outputs are const in ComputeResiduals()
- Document assumptions: Specify which solution branch (e.g., positive root)
Common Patterns
Iterative Solvers
double x = inputs.at("x")(0);
double y_guess = 1.0;
for (int iter = 0; iter < max_iterations; ++iter) {
double residual = ComputeR(x, y_guess);
double jacobian = ComputedR_dy(x, y_guess);
y_guess = y_guess - residual / jacobian;
if (std::abs(residual) < tolerance) {
break;
}
}
outputs.at("y")(0) = y_guess;
}
Systems of Equations
void Setup() override {
AddInput("a", {1}, "m");
AddInput("b", {1}, "m");
AddOutput("x", {1}, "m");
AddOutput("y", {1}, "m");
}
double a = inputs.at("a")(0);
double b = inputs.at("b")(0);
double x = outputs.at("x")(0);
double y = outputs.at("y")(0);
residuals.at("x")(0) = f1(a, b, x, y);
residuals.at("y")(0) = f2(a, b, x, y);
}
Verifying Solutions
Always verify that your solution satisfies the residual:
for (const auto& [name, var] : outputs) {
}
ComputeResiduals(inputs, outputs, residuals);
for (const auto& [name, var] : residuals) {
assert(std::abs(var(0)) < 1e-10);
}
A class for storing continuous and discrete variables.
Definition variable.h:85
Handling Cancellation
For long-running implicit solvers, disciplines can detect client cancellations by checking IsCancelled():
int max_iter = 10000;
double tolerance = 1e-10;
for (int iter = 0; iter < max_iter; iter++) {
throw std::runtime_error("Solver cancelled by client");
}
if (residual_norm < tolerance) break;
}
}
};
bool IsCancelled() const noexcept
Check if the current operation has been cancelled.
Key Points:
- Server automatically detects cancellations -
IsCancelled() is optional
- Particularly useful for iterative solvers that may not converge
- Check periodically to minimize overhead
- Works across all client languages
- See Explicit Disciplines for more cancellation examples
Complete Examples
See the examples directory:
examples/quadratic/ - Quadratic equation solver
See Also