Philote-Cpp
C++ bindings for the Philote MDO standard
Loading...
Searching...
No Matches
Best Practices and Limitations

Best Practices

Discipline Development

Always Override Compute Methods

The Compute() and ComputePartials() (explicit) or ComputeResiduals() and SolveResiduals() (implicit) methods have no default implementations:

// Required for explicit disciplines
void Compute(const philote::Variables &inputs,
philote::Variables &outputs) override {
// Must implement - no default
}
// Required for implicit disciplines
void ComputeResiduals(const philote::Variables &inputs,
const philote::Variables &outputs,
philote::Variables &residuals) override {
// Must implement - no default
}
void SolveResiduals(const philote::Variables &inputs,
philote::Variables &outputs) override {
// Must implement - no default
}
std::map< std::string, philote::Variable > Variables
Definition variable.h:404

Call Parent Initialize

When overriding Initialize(), always call the parent version first:

void Initialize() override {
ExplicitDiscipline::Initialize(); // Call parent first
AddOption("my_option", "float");
}

Declare All Partials

Use DeclarePartials() for every gradient you compute:

void SetupPartials() override {
// Declare all output/input pairs
DeclarePartials("f", "x");
DeclarePartials("f", "y");
// For implicit: also declare output/output pairs
DeclarePartials("residual", "output");
}

Variable Handling

Check Variable Dimensions

Always verify shapes match your expectations:

void Compute(const philote::Variables &inputs,
philote::Variables &outputs) override {
const auto& vec = inputs.at("vec");
assert(vec.Size() == expected_size); // Verify size
// Now safe to access
for (size_t i = 0; i < vec.Size(); ++i) {
outputs.at("result")(i) = process(vec(i));
}
}

Use Meaningful Units

Provide physical units for all variables:

// Good
AddInput("pressure", {1}, "Pa");
AddInput("temperature", {1}, "K");
AddInput("length", {1}, "m");
// Avoid
AddInput("p", {1}, ""); // Unclear name and no units

Use Type-Safe Access

Prefer .at() over [] for bounds checking:

// Recommended - throws exception if key doesn't exist
double x = inputs.at("x")(0);
// Avoid - creates entry if key doesn't exist
double x = inputs["x"](0); // May hide errors

Error Handling

Wrap Discipline Logic

Wrap computations in try-catch blocks:

void Compute(const philote::Variables &inputs,
philote::Variables &outputs) override {
try {
double x = inputs.at("x")(0);
if (x < 0) {
throw std::runtime_error("x must be non-negative");
}
outputs.at("y")(0) = std::sqrt(x);
} catch (const std::exception& e) {
std::cerr << "Compute error: " << e.what() << std::endl;
throw; // Re-throw to propagate to client
}
}

Validate Inputs

Check input validity before computation:

void Compute(const philote::Variables &inputs,
philote::Variables &outputs) override {
double divisor = inputs.at("divisor")(0);
if (std::abs(divisor) < 1e-14) {
throw std::runtime_error("Division by zero");
}
outputs.at("result")(0) = inputs.at("numerator")(0) / divisor;
}

Client Usage

Follow Initialization Sequence

Always initialize clients in the correct order:

// Correct order
client.ConnectChannel(channel);
client.GetInfo();
client.Setup();
client.GetVariableDefinitions();
client.GetPartialDefinitions(); // Optional
// Incorrect - will fail
client.GetVariableDefinitions(); // Error: not initialized

Sequential Operations

Don't call client methods concurrently:

// Good - sequential
philote::Variables out1 = client.ComputeFunction(inputs1);
philote::Variables out2 = client.ComputeFunction(inputs2);
// Bad - concurrent (not supported)
std::thread t1([&]() { client.ComputeFunction(inputs1); });
std::thread t2([&]() { client.ComputeFunction(inputs2); });

Reuse Clients

Create clients once and reuse:

// Good - reuse client
client.ConnectChannel(channel);
client.GetInfo();
client.Setup();
for (int i = 0; i < 1000; ++i) {
// Reuse same client
philote::Variables outputs = client.ComputeFunction(inputs);
}
// Avoid - creates new client each time (expensive)
for (int i = 0; i < 1000; ++i) {
philote::ExplicitClient client; // Don't do this
// ...
}
void GetInfo()
Get the discipline info.
void GetVariableDefinitions()
Get the variable definitions from the server.
void Setup()
Setup the discipline.
Client class for calling a remote explicit discipline.
Definition explicit.h:418
Variables ComputeFunction(const Variables &inputs)
Calls the remote analysis server function evaluation via gRPC.
void ConnectChannel(std::shared_ptr< grpc::ChannelInterface > channel)
Connects the client stub to a gRPC channel.

Documentation

Document Assumptions

Clearly document assumptions in your code:

void SolveResiduals(const philote::Variables &inputs,
philote::Variables &outputs) override {
// Implementation...
}

Comment Complex Logic

Add comments for non-obvious computations:

void ComputePartials(const philote::Variables &inputs,
philote::Partials &partials) override {
double x = inputs.at("x")(0);
// Chain rule: d/dx[f(g(x))] = f'(g(x)) * g'(x)
double inner = g(x);
double df_dg = f_prime(inner);
double dg_dx = g_prime(x);
partials[{"f", "x"}](0) = df_dg * dg_dx;
}
std::map< std::pair< std::string, std::string >, philote::Variable > Partials
Definition variable.h:405

Limitations

No Concurrent RPC Support

The library currently supports only one RPC call at a time:

// Not supported - concurrent calls
std::thread t1([&]() {
client1.ComputeFunction(inputs);
});
std::thread t2([&]() {
server.ProcessRequest(); // May conflict with t1
});
// Workaround - use separate servers/clients
// Each connects to different server instances

Single Server Instance

Each discipline should be hosted on a single server instance:

// Good - one discipline, one server
MyDiscipline discipline;
grpc::ServerBuilder builder;
discipline.RegisterServices(builder);
auto server = builder.BuildAndStart();
// Avoid - multiple servers for same discipline (not tested)
grpc::ServerBuilder builder1, builder2;
discipline.RegisterServices(builder1);
discipline.RegisterServices(builder2); // Undefined behavior

Sequential Client Operations

All client operations must be called sequentially:

// Correct
client.GetInfo();
client.Setup();
// Incorrect - parallel initialization
std::async(std::launch::async, [&](){ client.GetInfo(); });
std::async(std::launch::async, [&](){ client.Setup(); }); // Will fail

No Auto-Reconnect

If the connection drops, create a new client:

try {
philote::Variables outputs = client.ComputeFunction(inputs);
} catch (const std::exception& e) {
// Connection dropped - create new client
client.ConnectChannel(new_channel);
client.GetInfo();
client.Setup();
// Retry
outputs = client.ComputeFunction(inputs);
}

Variable Shape Constraints

Variable shapes are fixed after Setup():

void Setup() override {
AddInput("x", {10}, "m"); // Fixed size of 10
}
void Compute(const philote::Variables &inputs,
philote::Variables &outputs) override {
// Input "x" will always have size 10
// Cannot dynamically change shape
}

Performance Considerations

Minimize Data Transfers

Reduce the number of RPC calls:

// Good - batch inputs
inputs["x"] = philote::Variable(philote::kInput, {100}); // 100 values
philote::Variables outputs = client.ComputeFunction(inputs);
// Avoid - multiple calls for each value
for (int i = 0; i < 100; ++i) {
inputs["x"](0) = values[i];
client.ComputeFunction(inputs); // 100 RPCs instead of 1
}
A class for storing continuous and discrete variables.
Definition variable.h:85

Preallocate Variables

Preallocate variables instead of reallocating:

// Good - allocate once
inputs["x"] = philote::Variable(philote::kInput, {1});
for (double val : values) {
inputs.at("x")(0) = val; // Reuse same variable
client.ComputeFunction(inputs);
}
// Avoid - reallocate every iteration
for (double val : values) {
philote::Variables inputs; // Reallocates every time
inputs["x"] = philote::Variable(philote::kInput, {1});
inputs.at("x")(0) = val;
client.ComputeFunction(inputs);
}

Use Efficient Data Structures

Choose appropriate shapes for your data:

// Good for large sequential data
AddInput("time_series", {10000}, "s");
// Good for matrix operations
AddInput("matrix", {100, 100}, "m");
// Avoid deeply nested structures
AddInput("tensor", {10, 10, 10, 10, 10}, "m"); // May be inefficient

Common Pitfalls

Forgetting to Declare Partials

// Wrong - computes partial but didn't declare it
void SetupPartials() override {
// Forgot to declare!
}
void ComputePartials(const philote::Variables &inputs,
philote::Partials &partials) override {
partials[{"f", "x"}](0) = 2.0; // Error: not declared
}
// Correct
void SetupPartials() override {
DeclarePartials("f", "x"); // Must declare first
}

Modifying Const Inputs

// Wrong - inputs are const
void Compute(const philote::Variables &inputs,
philote::Variables &outputs) override {
inputs.at("x")(0) = 5.0; // Compile error: inputs is const
}

Incorrect Variable Types

// Wrong - using kOutput for inputs
inputs["x"] = philote::Variable(philote::kOutput, {1}); // Should be kInput
// Correct
inputs["x"] = philote::Variable(philote::kInput, {1});

Debugging Tips

Enable Verbose Logging

// Enable gRPC logging
setenv("GRPC_VERBOSITY", "DEBUG", 1);
setenv("GRPC_TRACE", "all", 1);

Verify Variable Metadata

void Setup() override {
AddInput("x", {10}, "m");
// Debug: print metadata
for (const auto& var : var_meta()) {
std::cout << "Variable: " << var.name()
<< ", Size: " << var.shape(0)
<< ", Units: " << var.units() << std::endl;
}
}

Check Gradient Accuracy

Use finite differences to verify analytic gradients:

double eps = 1e-7;
double x = inputs.at("x")(0);
// Analytic gradient
ComputePartials(inputs, partials);
double analytic = partials[{"f", "x"}](0);
// Finite difference
inputs.at("x")(0) = x + eps;
Compute(inputs, out_plus);
inputs.at("x")(0) = x - eps;
Compute(inputs, out_minus);
double numeric = (out_plus.at("f")(0) - out_minus.at("f")(0)) / (2 * eps);
std::cout << "Analytic: " << analytic << ", Numeric: " << numeric << std::endl;

See Also