C++ में ग्राफ़ बनाना

C++ ग्राफ़ बिल्डर इन कामों के लिए एक बेहतरीन टूल है:

  • जटिल ग्राफ़ बनाना
  • ग्राफ़ के पैरामीटर सेट करना (उदाहरण के लिए, InferenceCalculator पर किसी प्रतिनिधि को सेट करना, ग्राफ़ के हिस्सों को चालू/बंद करना)
  • ग्राफ़ की डुप्लीकेट कॉपी बनाना (उदाहरण के लिए, pbtxt में सीपीयू और जीपीयू के हिसाब से बनाए गए ग्राफ़ के बजाय, आपके पास एक ही कोड हो सकता है, जो ज़रूरी ग्राफ़ बनाता हो और जितना हो सके उतना शेयर करता हो)
  • वैकल्पिक ग्राफ़ इनपुट/आउटपुट के साथ काम करना
  • हर प्लैटफ़ॉर्म के हिसाब से ग्राफ़ बनाना

बुनियादी इस्तेमाल

आइए देखते हैं कि सरल ग्राफ़ के लिए, C++ ग्राफ़ बिल्डर का इस्तेमाल कैसे किया जा सकता है:

# Graph inputs.
input_stream: "input_tensors"
input_side_packet: "model"

# Graph outputs.
output_stream: "output_tensors"

node {
  calculator: "InferenceCalculator"
  input_stream: "TENSORS:input_tensors"
  input_side_packet: "MODEL:model"
  output_stream: "TENSORS:output_tensors"
  options: {
    [drishti.InferenceCalculatorOptions.ext] {
      # Requesting GPU delegate.
      delegate { gpu {} }
    }
  }
}

ऊपर दिया गया CalculatorGraphConfig बनाने का फ़ंक्शन ऐसा दिख सकता है:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  // Requesting GPU delegate.
  inference_opts.mutable_delegate()->mutable_gpu();
  input_tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  Stream<std::vector<Tensor>> output_tensors =
      inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

कम शब्दों में जवाब:

  • Stream/SidePacket के तौर पर ग्राफ़ इनपुट पाने के लिए, Graph::In/SideIn का इस्तेमाल करें
  • Stream/SidePacket के तौर पर नोड आउटपुट पाने के लिए Node::Out/SideOut का इस्तेमाल करें
  • स्ट्रीम और साइड पैकेट को नोड (Node::In/SideIn) और ग्राफ़ आउटपुट (Graph::Out/SideOut) से कनेक्ट करने के लिए, Stream/SidePacket::ConnectTo का इस्तेमाल करें
    • एक "शॉर्टकट" ऑपरेटर >> है, जिसका इस्तेमाल ConnectTo फ़ंक्शन के बजाय किया जा सकता है (जैसे, x >> node.In("IN")).
  • Stream/SidePacket::Cast का इस्तेमाल, AnyType (जैसे कि Stream<AnyType> in = graph.In(0);) के स्ट्रीम या साइड पैकेट को किसी खास टाइप पर कास्ट करने के लिए किया जाता है
    • AnyType के बजाय असल टाइप का इस्तेमाल करने से, आपको ग्राफ़ बिल्डर की सुविधाओं को ऐक्सेस करने और ग्राफ़ को पढ़ने में आसानी होगी.

बेहतर इस्तेमाल

उपयोगिता फ़ंक्शन

एक खास यूटिलिटी फ़ंक्शन में, अनुमान बनाने वाले कोड का डेटा निकालें, ताकि कोड को आसानी से पढ़ा जा सके और उसका दोबारा इस्तेमाल किया जा सके:

// Updates graph to run inference.
Stream<std::vector<Tensor>> RunInference(
    Stream<std::vector<Tensor>> tensors, SidePacket<TfLiteModelPtr> model,
    const InferenceCalculatorOptions::Delegate& delegate, Graph& graph) {
  auto& inference_node = graph.AddNode("InferenceCalculator");
  auto& inference_opts =
      inference_node.GetOptions<InferenceCalculatorOptions>();
  *inference_opts.mutable_delegate() = delegate;
  tensors.ConnectTo(inference_node.In("TENSORS"));
  model.ConnectTo(inference_node.SideIn("MODEL"));
  return inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<std::vector<Tensor>> input_tensors =
      graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
  SidePacket<TfLiteModelPtr> model =
      graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();

  InferenceCalculatorOptions::Delegate delegate;
  delegate.mutable_gpu();
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);

  // Graph outputs.
  output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));

  return graph.GetConfig();
}

इस वजह से, RunInference एक साफ़ इंटरफ़ेस उपलब्ध कराता है, जिसमें यह बताया जाता है कि इनपुट/आउटपुट क्या हैं और उनके टाइप क्या हैं.

इसका इस्तेमाल आसानी से किया जा सकता है. उदाहरण के लिए, अगर आपको एक अतिरिक्त मॉडल अनुमान चलाना है, तो सिर्फ़ कुछ लाइनों का ही इस्तेमाल किया जा सकता है:

  // Run first inference.
  Stream<std::vector<Tensor>> output_tensors =
      RunInference(input_tensors, model, delegate, graph);
  // Run second inference on the output of the first one.
  Stream<std::vector<Tensor>> extra_output_tensors =
      RunInference(output_tensors, extra_model, delegate, graph);

साथ ही, आपको नामों और टैग (InferenceCalculator, TENSORS, MODEL) के डुप्लीकेट वर्शन बनाने या यहां खास कॉन्सटेंट जोड़ने की ज़रूरत नहीं है - उन जानकारी को RunInference फ़ंक्शन के लिए स्थानीय भाषा में बदल दिया जाता है.

बिजली, पानी जैसी सुविधाओं की क्लास

निश्चित तौर पर, यह सिर्फ़ फ़ंक्शन के बारे में नहीं है. कुछ मामलों में, यूटिलिटी क्लास बनाना भी फ़ायदेमंद होता है. इससे ग्राफ़ बनाने के कोड को ज़्यादा पढ़ने लायक बनाने और गड़बड़ी की संभावना कम होती है.

MediaPipe PassThroughCalculator कैलकुलेटर ऑफ़र करता है, जो बस अपने इनपुट को पास कर रहा है:

input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"

output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"

node {
  calculator: "PassThroughCalculator"
  input_stream: "float_value"
  input_stream: "int_value"
  input_stream: "bool_value"
  # The order must be the same as for inputs (or you can use explicit indexes)
  output_stream: "passed_float_value"
  output_stream: "passed_int_value"
  output_stream: "passed_bool_value"
}

ऊपर दिया गया ग्राफ़ बनाने के लिए, सरल C++ कंस्ट्रक्शन कोड देखते हैं:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  auto& pass_node = graph.AddNode("PassThroughCalculator");
  float_value.ConnectTo(pass_node.In("")[0]);
  int_value.ConnectTo(pass_node.In("")[1]);
  bool_value.ConnectTo(pass_node.In("")[2]);
  Stream<float> passed_float_value = pass_node.Out("")[0].Cast<float>();
  Stream<int> passed_int_value = pass_node.Out("")[1].Cast<int>();
  Stream<bool> passed_bool_value = pass_node.Out("")[2].Cast<bool>();

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

pbtxt प्रज़ेंटेशन में गड़बड़ी का खतरा हो सकता है (जब हमारे पास पास करने के लिए कई इनपुट होते हैं), लेकिन C++ कोड और भी खराब दिखता है: दोहराए जाने वाले खाली टैग और Cast कॉल. आइए, देखते हैं कि किस तरह हम PassThroughNodeBuilder की शुरुआत करके बेहतर काम कर सकते हैं:

class PassThroughNodeBuilder {
 public:
  explicit PassThroughNodeBuilder(Graph& graph)
      : node_(graph.AddNode("PassThroughCalculator")) {}

  template <typename T>
  Stream<T> PassThrough(Stream<T> stream) {
    stream.ConnectTo(node_.In(index_));
    return node_.Out(index_++).Cast<T>();
  }

 private:
  int index_ = 0;
  GenericNode& node_;
};

और अब ग्राफ़ कंस्ट्रक्शन कोड ऐसा दिख सकता है:

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Graph inputs.
  Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
  Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
  Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();

  PassThroughNodeBuilder pass_node_builder(graph);
  Stream<float> passed_float_value = pass_node_builder.PassThrough(float_value);
  Stream<int> passed_int_value = pass_node_builder.PassThrough(int_value);
  Stream<bool> passed_bool_value = pass_node_builder.PassThrough(bool_value);

  // Graph outputs.
  passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
  passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
  passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));

  // Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
  return graph.GetConfig();
}

अब आपके पास अपने कंस्ट्रक्शन कोड में गलत ऑर्डर या इंडेक्स नहीं हो सकता. साथ ही, PassThrough इनपुट से Cast के टाइप का अनुमान लगाकर टाइपिंग सेव करने में मदद मिलेगी.

क्या करें और क्या न करें

अगर हो सके, तो शुरुआत में ही ग्राफ़ इनपुट तय करें

नीचे दिए गए कोड में:

  • यह अनुमान लगाना मुश्किल हो सकता है कि ग्राफ़ में आपके कितने इनपुट हैं.
  • क्या आने वाले समय में गड़बड़ी की आशंका और उसे मैनेज करना मुश्किल हो सकता है (जैसे, क्या यह इंडेक्स सही है? नाम? अगर कुछ इनपुट हटा दिए जाएं या ज़रूरी न कर दिए जाएं, तो क्या होगा? वगैरह).
  • RunSomething का फिर से इस्तेमाल नहीं किया जा सकता, क्योंकि अन्य ग्राफ़ में अलग-अलग इनपुट हो सकते हैं

DON'T — खराब कोड का उदाहरण.

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Graph& graph) {
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();  // Bad.
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  // 10/100/N lines of code.
  Stream<B> b = graph.In(1).SetName("b").Cast<B>()  // Bad.
  Stream<D> d = RunSomething(a, b, graph);
  // ...

  return graph.GetConfig();
}

इसके बजाय, अपने ग्राफ़ बिल्डर की शुरुआत में ही ग्राफ़ इनपुट तय करें:

DO — अच्छे कोड का उदाहरण.

Stream<D> RunSomething(Stream<A> a, Stream<B> b, Stream<C> c, Graph& graph) {
  // ...
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).SetName("a").Cast<A>();
  Stream<B> b = graph.In(1).SetName("b").Cast<B>();
  Stream<C> c = graph.In(2).SetName("c").Cast<C>();

  // 10/100/N lines of code.
  Stream<D> d = RunSomething(a, b, c, graph);
  // ...

  return graph.GetConfig();
}

अगर आपके पास ऐसा इनपुट स्ट्रीम या साइड पैकेट है जो हमेशा तय नहीं किया जाता है, तो std::optional का इस्तेमाल करें और इसे शुरुआत में रखें:

DO — अच्छे कोड का उदाहरण.

std::optional<Stream<A>> a;
if (needs_a) {
  a = graph.In(0).SetName(a).Cast<A>();
}

सबसे आखिर में ग्राफ़ आउटपुट तय करना

नीचे दिए गए कोड में:

  • साथ ही, यह अनुमान लगाना मुश्किल हो सकता है कि ग्राफ़ में कितने आउटपुट हैं.
  • क्या आने वाले समय में गड़बड़ी की संभावना और उसे मैनेज करना मुश्किल हो सकता है (जैसे कि क्या यह इंडेक्स सही इंडेक्स है? नाम? अगर कुछ आउटप हटा दिए जाएं या उन्हें वैकल्पिक बना दिया जाए, तो क्या होगा? वगैरह).
  • RunSomething का फिर से इस्तेमाल नहीं किया जा सकता, क्योंकि दूसरे ग्राफ़ में अलग-अलग आउटपुट हो सकते हैं

DON'T — खराब कोड का उदाहरण.

void RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  node.Out("OUTPUT_F")
      .SetName("output_f").ConnectTo(graph.Out(2));  // Bad.
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  node.Out("OUTPUT_D")
      .SetName("output_d").ConnectTo(graph.Out(0));  // Bad.
  // 10/100/N lines of code.
  node.Out("OUTPUT_E")
      .SetName("output_e").ConnectTo(graph.Out(1));  // Bad.
  // 10/100/N lines of code.
  RunSomething(input, graph);
  // ...

  return graph.GetConfig();
}

इसके बजाय, अपने ग्राफ़ बिल्डर के आखिर में ग्राफ़ के आउटपुट तय करें:

DO — अच्छे कोड का उदाहरण.

Stream<F> RunSomething(Stream<Input> input, Graph& graph) {
  // ...
  return node.Out("OUTPUT_F").Cast<F>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // 10/100/N lines of code.
  Stream<D> d = node.Out("OUTPUT_D").Cast<D>();
  // 10/100/N lines of code.
  Stream<E> e = node.Out("OUTPUT_E").Cast<E>();
  // 10/100/N lines of code.
  Stream<F> f = RunSomething(input, graph);
  // ...

  // Outputs.
  d.SetName("output_d").ConnectTo(graph.Out(0));
  e.SetName("output_e").ConnectTo(graph.Out(1));
  f.SetName("output_f").ConnectTo(graph.Out(2));

  return graph.GetConfig();
}

नोड को एक-दूसरे से अलग करके रखना

MediaPipe में, पैकेट स्ट्रीम और साइड पैकेट, प्रोसेसिंग नोड की तरह ही काम के होते हैं. नोड इनपुट ज़रूरतों और आउटपुट प्रॉडक्ट के बारे में साफ़ तौर पर और अलग-अलग जानकारी दी जाती है.

DON'T — खराब कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));

  auto& node2 = graph.AddNode("Calculator2");
  node1.Out("OUTPUT").ConnectTo(node2.In("INPUT"));  // Bad.

  auto& node3 = graph.AddNode("Calculator3");
  node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C"));  // Bad.

  auto& node4 = graph.AddNode("Calculator4");
  node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B"));  // Bad.
  node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C"));  // Bad.
  node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D"));  // Bad.

  // Outputs.
  node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0));  // Bad.
  node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1));  // Bad.
  node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2));  // Bad.
  node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3));  // Bad.

  return graph.GetConfig();
}

ऊपर दिए गए कोड में:

  • नोड एक-दूसरे से जुड़े होते हैं. उदाहरण के लिए, node4 को पता है कि इसके इनपुट कहां से आ रहे हैं (node1, node2, node3) और इससे रीफ़ैक्टरिंग, रखरखाव, और कोड का दोबारा इस्तेमाल करने में मुश्किल हो रही है
    • इस्तेमाल का ऐसा पैटर्न, प्रोटो प्रज़ेंटेशन से डाउनग्रेड होता है, जहां नोड डिफ़ॉल्ट रूप से डिकोड हो जाते हैं.
  • node#.Out("OUTPUT") कॉल डुप्लीकेट होते हैं और कॉल को पढ़ने में समस्या आती है. ऐसा इसलिए होता है, क्योंकि इसके बजाय साफ़ नामों का इस्तेमाल किया जा सकता है और कॉल का टाइप भी दिया जा सकता है.

इसलिए, ऊपर दी गई समस्याओं को ठीक करने के लिए, ग्राफ़ बनाने के लिए यह कोड लिखा जा सकता है:

DO — अच्छे कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  // `node1` usage is limited to 3 lines below.
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  // `node2` usage is limited to 3 lines below.
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  // `node3` usage is limited to 4 lines below.
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  // `node4` usage is limited to 5 lines below.
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

अब, ज़रूरत पड़ने पर, node1 को आसानी से हटाया जा सकता है और b को एक ग्राफ़ इनपुट बनाया जा सकता है. साथ ही, node2, node3, node4 के लिए किसी अपडेट की ज़रूरत नहीं होती है (जैसा कि प्रोटो प्रज़ेंटेशन में होता है), क्योंकि उन्हें एक-दूसरे से अलग किया जाता है.

कुल मिलाकर, ऊपर दिया गया कोड प्रोटो ग्राफ़ को ज़्यादा बारीकी से कॉपी करता है:

input_stream: "a"

node {
  calculator: "Calculator1"
  input_stream: "INPUT:a"
  output_stream: "OUTPUT:b"
}

node {
  calculator: "Calculator2"
  input_stream: "INPUT:b"
  output_stream: "OUTPUT:C"
}

node {
  calculator: "Calculator3"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  output_stream: "OUTPUT:d"
}

node {
  calculator: "Calculator4"
  input_stream: "INPUT_B:b"
  input_stream: "INPUT_C:c"
  input_stream: "INPUT_D:d"
  output_stream: "OUTPUT:e"
}

output_stream: "b"
output_stream: "c"
output_stream: "d"
output_stream: "e"

साथ ही, अब दूसरे ग्राफ़ में फिर से इस्तेमाल करने के लिए, यूटिलिटी फ़ंक्शन एक्सट्रैक्ट किए जा सकते हैं:

DO — अच्छे कोड का उदाहरण.

Stream<B> RunCalculator1(Stream<A> a, Graph& graph) {
  auto& node = graph.AddNode("Calculator1");
  a.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<B>();
}

Stream<C> RunCalculator2(Stream<B> b, Graph& graph) {
  auto& node = graph.AddNode("Calculator2");
  b.ConnectTo(node.In("INPUT"));
  return node.Out("OUTPUT").Cast<C>();
}

Stream<D> RunCalculator3(Stream<B> b, Stream<C> c, Graph& graph) {
  auto& node = graph.AddNode("Calculator3");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  return node.Out("OUTPUT").Cast<D>();
}

Stream<E> RunCalculator4(Stream<B> b, Stream<C> c, Stream<D> d, Graph& graph) {
  auto& node = graph.AddNode("Calculator4");
  b.ConnectTo(node.In("INPUT_B"));
  c.ConnectTo(node.In("INPUT_C"));
  d.ConnectTo(node.In("INPUT_D"));
  return node.Out("OUTPUT").Cast<E>();
}

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

कॉन्टेंट को बेहतर तरीके से पढ़ने के लिए, नोड को अलग-अलग करना

DON'T — खराब कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();
  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();
  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();
  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();
  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();
  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

ऊपर दिए गए कोड में, यह समझना मुश्किल हो सकता है कि हर नोड कहां से शुरू और खत्म होता है. इसे बेहतर बनाने और अपने कोड रीडर में मदद करने के लिए, हर नोड के पहले और बाद में खाली लाइनें रखें:

DO — अच्छे कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  auto& node1 = graph.AddNode("Calculator1");
  a.ConnectTo(node1.In("INPUT"));
  Stream<B> b = node1.Out("OUTPUT").Cast<B>();

  auto& node2 = graph.AddNode("Calculator2");
  b.ConnectTo(node2.In("INPUT"));
  Stream<C> c = node2.Out("OUTPUT").Cast<C>();

  auto& node3 = graph.AddNode("Calculator3");
  b.ConnectTo(node3.In("INPUT_B"));
  c.ConnectTo(node3.In("INPUT_C"));
  Stream<D> d = node3.Out("OUTPUT").Cast<D>();

  auto& node4 = graph.AddNode("Calculator4");
  b.ConnectTo(node4.In("INPUT_B"));
  c.ConnectTo(node4.In("INPUT_C"));
  d.ConnectTo(node4.In("INPUT_D"));
  Stream<E> e = node4.Out("OUTPUT").Cast<E>();

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}

साथ ही, ऊपर दिया गया प्रतिनिधित्व CalculatorGraphConfig प्रोटो प्रतिनिधित्व से बेहतर मेल खाता है.

अगर यूटिलिटी फ़ंक्शन में नोड एक्सट्रैक्ट किए जाते हैं, तो वे पहले से ही फ़ंक्शन के दायरे में आते हैं और यह भी साफ़ तौर पर पता चलता है कि वे कहां से शुरू और खत्म होते हैं. इसलिए, इसे इस्तेमाल करना पूरी तरह से सही है:

DO — अच्छे कोड का उदाहरण.

CalculatorGraphConfig BuildGraph() {
  Graph graph;

  // Inputs.
  Stream<A> a = graph.In(0).Cast<A>();

  Stream<B> b = RunCalculator1(a, graph);
  Stream<C> c = RunCalculator2(b, graph);
  Stream<D> d = RunCalculator3(b, c, graph);
  Stream<E> e = RunCalculator4(b, c, d, graph);

  // Outputs.
  b.SetName("b").ConnectTo(graph.Out(0));
  c.SetName("c").ConnectTo(graph.Out(1));
  d.SetName("d").ConnectTo(graph.Out(2));
  e.SetName("e").ConnectTo(graph.Out(3));

  return graph.GetConfig();
}