Introduction
Explicit disciplines compute direct function evaluations (f(x) = y) and optionally their gradients. They are suitable for analyses where outputs can be computed directly from inputs without solving implicit equations.
Creating a Simple Explicit Discipline
Here's a minimal explicit discipline:
private:
}
}
double x = inputs.at("x")(0);
outputs.at("y")(0) = 2.0 * x + 1.0;
}
partials[{"y", "x"}](0) = 2.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.
void DeclarePartials(const std::string &f, const std::string &x)
Declare a (set of) partial(s) for the discipline.
Explicit discipline class.
Definition explicit.h:263
virtual void Compute(const philote::Variables &inputs, philote::Variables &outputs)
Function evaluation for the discipline.
virtual void ComputePartials(const philote::Variables &inputs, Partials &partials)
Gradient evaluation 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
Multi-Input/Output Discipline
A more complex example with multiple inputs and outputs:
#include <cmath>
private:
void Setup() override {
}
void SetupPartials() override {
}
double x = inputs.at("x")(0);
double y = inputs.at("y")(0);
outputs.at("f_xy")(0) = std::pow(x - 3.0, 2.0) + x * y +
std::pow(y + 4.0, 2.0) - 3.0;
}
double x = inputs.at("x")(0);
double y = inputs.at("y")(0);
partials[{"f_xy", "x"}](0) = 2.0 * x - 6.0 + y;
partials[{"f_xy", "y"}](0) = 2.0 * y + 8.0 + x;
}
};
Definition paraboloid_server.cpp:48
Discipline with Options
Disciplines can define configurable options:
private:
double scale_factor_ = 1.0;
}
}
}
outputs.at("y")(0) = scale_factor_ * inputs.at("x")(0);
}
};
virtual void Initialize()
Initialize function that sets up available options.
virtual void Configure()
Configure function that is called after options are set.
void AddOption(const std::string &name, const std::string &type)
Add an option to the discipline.
Starting a Server
Once you've defined your discipline, create a server to host it:
#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
Setup()
Called once to define variables:
void Setup() override {
AddInput("name", {shape}, "units");
AddOutput("name", {shape}, "units");
}
Compute()
Must be implemented - performs the actual computation:
double x = inputs.at("x")(0);
outputs.at("y")(0) = f(x);
}
SetupPartials() and ComputePartials()
Optional, but recommended for optimization:
void SetupPartials() override {
DeclarePartials("output", "input");
}
partials[{"output", "input"}](0) = df_dx;
}
Lifecycle
The discipline lifecycle when a client connects:
- Initialize() - Define available options
- SetOptions() - Client sets option values
- Configure() - Process option values
- Setup() - Define variables
- SetupPartials() - Declare available gradients
- Compute() - Called for each function evaluation
- ComputePartials() - Called for each gradient evaluation
Best Practices
- Always override Compute(): No default implementation exists
- Call parent Initialize(): When overriding, call
ExplicitDiscipline::Initialize() first
- Declare all partials: Use
DeclarePartials() for every gradient you compute
- Use const for inputs: Input variables should not be modified
- Preallocate outputs: Framework preallocates based on
Setup()
- Handle exceptions: Wrap computation in try-catch for robustness
Common Patterns
Vector Operations
void Setup() override {
AddInput("vec", {10}, "m");
AddOutput("scaled", {10}, "m");
}
for (size_t i = 0; i < 10; ++i) {
outputs.at("scaled")(i) = 2.0 * inputs.at("vec")(i);
}
}
Matrix Operations
void Setup() override {
AddInput("matrix", {3, 3}, "m");
AddOutput("determinant", {1}, "m**3");
}
const auto& A = inputs.at("matrix");
outputs.at("determinant")(0) = ComputeDeterminant(A);
}
Handling Cancellation
For long-running computations, disciplines can detect and respond to client cancellations (timeouts, disconnects) by checking IsCancelled():
for (int iter = 0; iter < 1000000; iter++) {
throw std::runtime_error("Computation cancelled by client");
}
}
outputs.at("result")(0) = computed_value;
}
};
bool IsCancelled() const noexcept
Check if the current operation has been cancelled.
Key Points:
- Server automatically detects cancellations -
IsCancelled() is optional
- Only use for computations that would benefit from early termination
- Check periodically (not every iteration) to minimize overhead
- Works across all client languages (Python, C++, etc.)
- Throw exception or return early when cancelled
Client-Side:
client.SetRPCTimeout(std::chrono::milliseconds(5000));
try {
auto outputs = client.ComputeFunction(inputs);
} catch (const std::runtime_error& e) {
}
Complete Examples
See the examples directory for complete working examples:
examples/paraboloid/ - Basic explicit discipline
examples/rosenbrock/ - Classic optimization test function
See Also