ساختن نمودارها در C++

گراف ساز C++ ابزاری قدرتمند برای موارد زیر است:

  • ساخت نمودارهای پیچیده
  • پارامترسازی نمودارها (مثلاً تنظیم یک نماینده در InferenceCalculator ، فعال/غیرفعال کردن بخش‌هایی از نمودار)
  • کپی کردن نمودارها (به عنوان مثال به جای نمودارهای اختصاصی CPU و GPU در 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();
}

خلاصه کوتاه:

  • از Graph::In/SideIn برای دریافت ورودی های نمودار به عنوان Stream/SidePacket استفاده کنید
  • از Node::Out/SideOut برای دریافت خروجی گره به عنوان Stream/SidePacket استفاده کنید
  • از Stream/SidePacket::ConnectTo برای اتصال جریان‌ها و بسته‌های جانبی به ورودی‌های گره ( Node::In/SideIn ) و خروجی‌های نمودار ( Graph::Out/SideOut ) استفاده کنید.
    • یک اپراتور "میانبر" >> وجود دارد که می توانید به جای تابع 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();
}

اکنون نمی‌توانید ترتیب یا فهرست نادرستی در کد ساخت‌وساز خود داشته باشید و با حدس زدن نوع Cast از ورودی PassThrough ، مقداری تایپ را ذخیره کنید.

بایدها و نبایدها

در صورت امکان ورودی های گراف را در همان ابتدا تعریف کنید

در کد زیر:

  • حدس زدن تعداد ورودی های شما در نمودار دشوار است.
  • آیا ممکن است به طور کلی مستعد خطا باشد و نگهداری آن در آینده سخت باشد (مثلاً آیا فهرست درست است؟ نام؟ اگر برخی از ورودی ها حذف شوند یا اختیاری شوند چه می شود؟ و غیره).
  • استفاده مجدد RunSomething محدود است زیرا نمودارهای دیگر ممکن است ورودی های متفاوتی داشته باشند

نکن - مثالی از کد بد.

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 محدود است زیرا نمودارهای دیگر ممکن است خروجی های متفاوتی داشته باشند

نکن - مثالی از کد بد.

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، جریان های بسته و بسته های جانبی به اندازه گره های پردازش معنادار هستند. و هرگونه نیاز ورودی گره و محصولات خروجی به طور واضح و مستقل بر حسب جریان ها و بسته های جانبی که مصرف و تولید می کند بیان می شود.

نکن - مثالی از کد بد.

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();
}

برای خوانایی بهتر گره ها را جدا کنید

نکن - مثالی از کد بد.

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();
}