0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

制御とインフラストラクチャの実装~連載 TSPソルバ実装記02~

Last updated at Posted at 2025-10-19

はじめに

前回の記事「連載 1: プロジェクトの全体像と設計原則」では, 長期的な拡張性と保守性を確保するため, レイヤードアーキテクチャに基づいたフレームワークの全体設計と, プロジェクト間の依存関係について解説しました.

本記事では, この設計に基づいたフレームワークの稼働に不可欠な「制御」と「インフラストラクチャ」のレイヤが, どのように実装されているかを具体的に見ていきます.

特に, 以下の 3 つの基盤が依存性逆転の原則をどのように実現しているのか, C#コードの視点から深く掘り下げます.

  • データと契約の具現化(Common): すべての層の土台となるインターフェースとデータモデルの詳細.
  • インフラストラクチャの実装(ExternalInterfaces): MLflow API との具体的な接続, そしてそのための DI(依存性注入)の仕組み.
  • 実験ワークフローの制御(Processor): 実験のライフサイクル全体を統括し, 結果を記録するコアロジック.

設計図から具体的な機能を構築するプロセスを, ぜひご覧ください.

連載の構成
本連載「TSP ソルバ実装記」は全 4 回の予定です.

目次

Common プロジェクト: データと契約の基盤

Commonプロジェクトは全てのレイヤが依存する共通の土台を提供します. 大きく分けて, データモデルの定義とレイヤ間を接続するインターフェイスの定義という 2 点に集約されます

  • DataModels
    • CalculatorModels
    • External
    • Reporter
    • Settings
  • Interfaces
    • MLflowInterfaces
    • Solver
    • SystemMonitorInterface

データモデルの定義と責務の分離

データモデルは, Common/DataModels 以下で一元管理されています.
この中で最も重要な設計原則は, データが純粋な計算に関するものか, 外部 I/O に関するものかで厳密に分離されている点です.

フォルダ 責務 分離する理由
CalculatorModels 純粋な計算に必要なデータ
(ProblemModel, CalculationResultなど)
Solver 層からのみ参照されるべき, TSP 問題の核心データ.
外部 API の都合に影響されない.
External 外部 I/O に必要なデータ
(ExperimentRequest, LogMetricsRequestなど)
ExternalInterfaces層との間でやり取りされるデータ.
MLflow のスキーマ変更に対応しやすい.
Reporter 進捗状況のレポーティング
(ProgressReport)
Processor層, Solver 層から参照されるべき, レポーティング用のデータ.
問題そのものや外部 API の都合に影響されない
Settings 計算実験に必要なデータ
(FixedSettings, ExperimentSettingsなど)
Processor層から参照されるべき, 実験設定のデータ.
外部 API の都合に影響されない.

インターフェースの定義

Commonプロジェクトのもう一つの核となるのが, Common/Interfaces以下で定義される各種インターフェースです.
これらは外部の具象クラス(ExternalInterfacesSolverの実装クラス)と上位の制御クラス(Processor)との間を取り持つ役割を担います.

インターフェイス 役割 依存性逆転の原則における位置づけ
ISolver TSP アルゴリズム(計算機)の抽象的な実行契約 ProcessorISolverを通じてSolver層の実装を呼び出す
IMLflowClient MLflow API とのデータ送受信の抽象的な契約 ProcessorIMLflowClientを通じてExternalInterfaces層の実装を呼び出す
AbstractSystemMonitor CPU/メモリなどのシステムリソース監視機能の抽象的な契約 外部環境に依存する具体的な監視ロジックをProcessorから隠蔽する

これらのインターフェースにより, Processorプロジェクトは実際にどのようなアルゴリズム(焼きなまし法, 遺伝的アルゴリズム, ...)や,
どのような通信クライアント(RealClient, MockClient)が使われているかを知る必要がありません.
これが依存性逆転の原則であり, DI(依存性注入)の土台となります.

ExternalInterfaces プロジェクト: インフラストラクチャとの接続

ExternalInterfaces プロジェクトはインフラストラクチャ層として, フレームワーク外部の具体的なサービス(MLflow API, システム監視)との接続を担います.

この層の核心は, Commonプロジェクトで定義されたインターフェース(抽象)を実装すること, そして, その実装を上位層(Processor)に渡すための DI の仕組みを提供することです.

DI の実現: ファクトリーパターンの採用

上位層であるProcessorが, ExternalInterfacesに含まれる具象クラス(RealClientなど)に直接依存することは, クリーンアーキテクチャの原則違反となります.
この依存を防ぎ, 実行時に適切なインスタンスを注入するため, ファクトリーパターンを採用しています.
以下が依存性注入(DI)の仕組みです

  • 契約の定義(Common)
    • CommonプロジェクトでIMLflowClientIMLflowClientFactoryというインターフェースを定義します.
  • 実装(ExternalInterfaces)
    • RealClientFactory.csIMLflowClientFactoryを実装.
    • RealClient.csIMLflowClientを実装.
  • 注入(Application/Processor)
    • Applicationプロジェクトで DI コンテナを構築する際, IMLflowClientFactoryが要求されたらRealClientFactoryのインスタンスを返すように登録します.

この仕組みにより, ProcessorIMLflowClientFactory(抽象)を要求するだけで, 実行時に RealClientFactory(具象)を受け取り, そのファクトリーを通じて RealClient(具象)のインスタンスを得ることができます.

WebAPI 通信の実装: RealClient

ExternalInterfaces/Mlflowフォルダには, 前作で開発した MLflow ラッパー API との具体的な通信を担うRealClient.csが格納されています.

  • 契約の遵守
    • RealClientCommonで定義されたIMLflowClientインターフェースを実装しており, Run の開始/終了, パラメータやメトリクスのロギングといったメソッドを提供します.
  • 通信ロジック
    • 各メソッドの内部では, Common/DataModels/Externalで定義されたリクエスト構造体を使用し, 前作の WebAPI への HTTP リクエストを具体的に組み立て, 送信します.

このように, ExternalInterfacesは具体的な通信の詳細やライブラリへの依存を自身の中に閉じ込める役割を果たします. これにより, 仮に MLflow の通信仕様が変わっても, RealClientの内部だけを修正すればよく, 上位のProcessor層に影響が及ぶことはありません.

image.png

システムモニタリング

ExternalInterfaces/SystemMonitorフォルダには, システムモニタリングを担うクラスを格納しています.
基底クラスはAbstractSystemMonitorです. LinuxやmacOSでも使用可能なCrossPlatformMonitor, Windowsのみ使用可能なWindowsMonitorを実装しました.

AbstractSystemMonitorIMLflowClientインターフェースを持たせることにより, 各サブクラスにおいてはIMLflowClientを知らない状況を作りました.

image.png

Processor プロジェクト: 実験ワークフローの制御

Processorプロジェクトは, アプリケーション層の核心であり,
前段で準備された抽象的な契約(ISolver, IMLflowClientなど)を統合し,
実験のライフサイクル全体を統括する責務を持ちます.

ProcessorComponentの役割とライフサイクルの統括

このプロジェクトの主役はProcessorComponent.csです.
このクラスが, 個々の実験の開始から終了までの全工程を制御するワークフローエンジンとして機能します.

ProcessorComponentが実行する主要なライフサイクルは以下の通りです.

  1. 設定読み込み: Common/Settingsから実験設定(JSONファイル)を読み込みます.
  2. 初期化: IMLflowClientFactoryなどを用いて, 必要な依存オブジェクトを初期化します.
  3. Run開始: IMLflowClientを通じてMLflowに実験開始を通知し, Run IDを取得します.
  4. ソルバ実行: ISolverインターフェースを通じてTSP計算を実行します.
  5. ロギング: 計算結果やシステムメトリクスをIMLflowClientに渡し, ロギングを行います.
  6. Run終了: IMLflowClientを通じてMLflowに実験終了を通知します.

DIによる統合と抽象インターフェースの利用

ProcessorComponentのコンストラクタには, 必要なすべての依存オブジェクトがインターフェースとして注入されます.

// 抽象的なインターフェースのみに依存
public class ProcessorComponent
{
    public ProcessorComponent(ISolver solver, IMLflowClientFactory clientFactory, AbstractSystemMonitor monitor, IConfiguration config)
    {
        // ... (依存性注入されたインスタンスを保持)
    }
    // ...
}

これにより, Processor層は具象クラス(RealClientや特定のソルバ実装)に一切依存しません.
たとえば, ロギングを行う際, Processorが知っているのはIMLflowClient.LogMetricsAsync()という契約(インターフェース)のみであり,
その背後で実際にHTTP通信が行われているかどうかは関知しません. この徹底した抽象化が, テストの容易性とコンポーネントの交換可能性を保証します.

非同期処理とIProgressによる進捗報告の実装

最適化アルゴリズムは実行時間が長くなる傾向があるため, フレームワークは長時間動作を前提とした設計が必要です.

  • 非同期処理
    • ProcessorComponent.ExecuteAsync()など, 主要なメソッドは全て非同期(async/await)で実装されています.
    • これにより, 実験の実行中もアプリケーション全体がブロックされるのを防ぎます.
  • IProgressによる進捗報告
    • Solver層で進行する計算の状況を, 制御層であるProcessorが受け取り->コンソールに出力するために, C#標準のIProgress<T>インターフェースを採用しています.
    • ProcessorComponentIProgress<ProgressReport>を実装し, ソルバに渡します.
    • Solverは計算の途中でIProgress.Report()を呼び出すだけで, 自身の進捗をProcessorに伝えることができます.
    • この設計により, 計算ロジック(Solver)はレポーティングの方法(コンソール出力か, GUI表示か)を知る必要がなく, 責務の分離が保たれます.

Application プロジェクト: アプリケーションの起動

Applicationプロジェクトは, フレームワークの最も外側にあるエントリポイントを担います.
これまでに定義してきた抽象化と具象化をDIコンテナを通じて結合し, フレームワーク全体を起動させることです.

この設計において, Applicationプロジェクトは極めてシンプルに保たれており,
依存性注入(DI)や依存性の解決といった複雑な処理を, Processorプロジェクト側に委譲している点が特徴です.

Prgram.csの役割と起動ロジック

Prgram.csの主な責務は以下の通りです.

  1. 設定ファイルのパス解決: コマンドライン引数から設定ファイルのパスを取得し, 引数がない場合はデフォルトパス(Data/Settings/settings.json)を使用します.
  2. ProcessorComponent.MainAsync()の呼び出し: Processorプロジェクトが提供する静的メソッドを呼び出し, アプリケーションの制御権を渡します.
  3. 致命的なエラーの捕捉: MainAsyncの実行中に発生した, アプリケーション起動に関わる致命的なエラーを捕捉し, ユーザーに分かりやすく表示します.

以下が実装コードです.

using Processor;
using System;

namespace Application
{
    public static class Prgram
    {
        static void Main(string[] args)
        {
            // 1. 設定ファイルのパス解決
            string settingsPath = args.Length > 0 ? args[0] : $"Data/Settings/settings.json";
            try
            {
                // 2. ProcessorComponent の起動(非同期メソッドの同期的な実行)
                ProcessorComponent.MainAsync(settingsPath).GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                // 3. 致命的なエラーの捕捉
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"\n[FATAL ERROR] Application failed to start or complete all runs: {ex.Message}");
                Console.ResetColor();
                Console.WriteLine($"Details: {ex}");
            }
        }
    }
}

制御の委譲(DIコンテナの隠蔽)

この実装の重要なポイントは, DIコンテナの構築や依存性の登録処理が, Applicationプロジェクトには現れていない点です.

  • ProcessorComponent.MainAsync() が, 内部で DI コンテナの構築, 依存性の解決, そして ProcessorComponent インスタンスの生成と実行を一括して担っています.
  • Application プロジェクトは, Processorプロジェクトにのみ依存し, DI フレームワークや ExternalInterfaces などの下位層の具体的なクラスを一切参照しないため, プロジェクト構造の純粋性が保たれています.

この設計により, 抽象化された設計図が初めて動作可能なアプリケーションとして統合されます.


まとめと次回予告: TSPソルバの抽象化へ

本記事では, 連載第1弾で設計したレイヤードアーキテクチャに基づき, フレームワークの制御とインフラストラクチャがどのように実装されているかを具体的に解説しました.

  • Common
    • データモデルの責務分離と, ISolver, IMLflowClientなどのインターフェースによる契約の確立を行いました.
  • ExternalInterfaces
    • ファクトリーパターンと DI の仕組みを採用し, WebAPI クライアント(RealClient)を実装することで, 外部依存性を上位層から完全に隔離しました.
  • Processor
    • ProcessorComponentが抽象インターフェースを通じて実験のライフサイクルを統括し, 非同期処理と IProgress で進捗報告を可能にしました.

これで, 外部サービスとの連携と実験の実行フローを司る基盤が完成しました.

次回の記事「連載3: ソルバの抽象化とTSPコアロジック」では, いよいよフレームワークの心臓部であるビジネスロジック層(Solver)に踏み込みます.

  • ソルバの抽象化: ISolverインターフェースをどのように実装し, アルゴリズムの実装を柔軟にするか.
  • コアロジックの分離: IEvaluatorといった補助インターフェースを導入し, ソルバの中核となる評価ロジックを分離する設計意図.

Appendix

Common プロジェクトの実装

DataModels

using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Common.DataModels.CalculatorModels
{
    /// <summary>
    /// アルゴリズムのモデルを表す列挙体
    /// </summary>
    public enum AlgorithmModels
    {
        /// <summary>
        /// アニーリングモデル
        /// </summary>
        AnnealingModel,

        /// <summary>
        /// 遺伝的アルゴリズムのモデル
        /// </summary>
        GeneticAlgorithm,

        /// <summary>
        /// アイランドモデル
        /// </summary>
        IslandModel,
    }

    /// <summary>
    /// 計算結果のデータモデル
    /// </summary>
    /// <value></value>
    public record CalculationResult
    {
        /// <summary>
        /// モデル名
        /// </summary>
        [JsonPropertyName("model_name")]
        public required string ModelName { get; init; }

        /// <summary>
        /// 問題
        /// </summary>
        [JsonPropertyName("problem")]
        public required ProblemModel Problem { get; init; }

        /// <summary>
        /// 計算にかかった時間
        /// </summary>
        [JsonPropertyName("calculation_time")]
        public required double CalculationTime { get; init; }

        /// <summary>
        /// ベストスコア
        /// </summary>
        [JsonPropertyName("best_score")]
        public required double BestDistance { get; init; }

        /// <summary>
        /// ベストスコアを取った時の都市順序
        /// </summary>
        [JsonPropertyName("tour")]
        public required List<int> Tour { get; init; }
    }

        /// <summary>
    /// TSPに用いる問題のモデル
    /// </summary>
    /// <value></value>
    public record ProblemModel
    {
        /// <summary>
        /// ベースファイルのパス
        /// </summary>
        [JsonPropertyName("base_file")]
        public required string BaseFile { get; init; }

        /// <summary>
        /// 各都市のIdとx座標, y座標
        /// </summary>
        [JsonPropertyName("coordinates")]
        public required Dictionary<int, Coordinate> Coordinates { get; init; }
    }

    /// <summary>
    /// 座標のオブジェクト
    /// </summary>
    public record Coordinate
    {
        /// <summary>
        /// x座標
        /// </summary>
        [JsonPropertyName("x")]
        public required double X { get; init; }

        /// <summary>
        /// y座標
        /// </summary>
        [JsonPropertyName("y")]
        public required double Y { get; init; }
    }
}
using System.Text.Json.Serialization;

namespace Common.DataModels.External
{
    /// <summary>
    /// 実験, Runを開始するときのリクエストモデル
    /// </summary>
    /// <value></value>
    public record ExperimentRequest
    {
        /// <summary>
        /// Run名
        /// </summary>
        [JsonPropertyName("run_name")]
        public required string RunName { get; init; }
    }
    /// <summary>
    /// 実験, Runを開始した後のレスポンスモデル
    /// </summary>
    /// <value></value>
    public record ExperimentResponse
    {
        /// <summary>
        /// Run Id
        /// </summary>
        [JsonPropertyName("run_id")]
        public required string RunId { get; init; }

        /// <summary>
        /// Experiment Id
        /// </summary>
        [JsonPropertyName("experiment_id")]
        public required string ExperimentId { get; init; }

        /// <summary>
        /// 実験開始結果のステータス
        /// </summary>
        [JsonPropertyName("status")]
        public required string Status { get; init; }
    }
        /// <summary>
    /// Runを終わらせるリクエスト
    /// </summary>
    public record TerminateRunRequest
    {
        /// <summary>
        /// 終了時のステータス
        /// </summary>
        [JsonPropertyName("status")]
        public required string Status { get; init; }
    }
}
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Common.DataModels.External
{
    /// <summary>
    /// パラメータ記録用のリクエスト
    /// </summary>
    public record LogParamsRequest
    {
        /// <summary>
        /// パラメータのリスト
        /// </summary>
        [JsonPropertyName("param_list")]
        public required List<ParamPair> ParamList { get; init; }
    }

    /// <summary>
    /// パラメータ記録用オブジェクト
    /// </summary>
    /// <value></value>
    public record ParamPair
    {
        /// <summary>
        /// パラメータの名前
        /// </summary>
        [JsonPropertyName("key")]
        public required string Key { get; init; }

        /// <summary>
        /// パラメータの値
        /// </summary>
        [JsonPropertyName("value")]
        public required string Value { get; init; }
    }

    /// <summary>
    /// メトリクス記録用のリクエスト
    /// </summary>
    public record LogMetricsRequest
    {
        /// <summary>
        /// メトリクスのリスト
        /// </summary>
        [JsonPropertyName("metric_list")]
        public required List<MetricPair> MetricList { get; init; }
    }

    /// <summary>
    /// メトリクス記録用オブジェクト
    /// </summary>
    public record MetricPair
    {
        /// <summary>
        /// メトリクスの名前
        /// </summary>
        [JsonPropertyName("key")]
        public required string Key { get; init; }

        /// <summary>
        /// メトリクスの値
        /// </summary>
        [JsonPropertyName("value")]
        public required double Value { get; init; }

        /// <summary>
        /// 時系列記録用のキー
        /// </summary>
        [JsonPropertyName("epoch")]
        public required int Epoch { get; init; }
    }

    /// <summary>
    /// ロギング結果のレスポンス
    /// </summary>
    public record LogResponse
    {
        /// <summary>
        /// ロギング結果
        /// </summary>
        [JsonPropertyName("status")]
        public required string Status { get; init; }
    }
}
namespace Common.DataModels.Reporter
{
    /// <summary>
    /// 進捗状況のレポート用
    /// </summary>
    /// <value></value>
    public record ProgressReport
    {
        public required int CurrentStep { get; init; }
        public required int TotalSteps { get; init; }
        public required string Message { get; init; }
    }
}
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Common.DataModels.Settings
{
    /// <summary>
    /// Settingsファイルのルート構造を定義するレコード
    /// </summary>
    public record RootSettings
    {
        [JsonPropertyName("fixed")]
        public required FixedSettings Fixed { get; init; }

        // experimentsセクションはExperimentSettingsのリスト
        [JsonPropertyName("experiments")]
        public required List<ExperimentSettings> Experiments { get; init; }
    }

    /// <summary>
    /// JSONの "fixed" セクションに対応する静的な設定レコード
    /// </summary>
    public record FixedSettings
    {
        [JsonPropertyName("project_name")]
        public required string ProjectName { get; init; }

        [JsonPropertyName("config_path")]
        public required string ConfigPath { get; init; }

        [JsonPropertyName("out_dir")]
        public required string OutDir { get; init; }
    }

    /// <summary>
    /// JSONの "experiments" 配下の一つの実験設定に対応するレコード
    /// </summary>
    public record ExperimentSettings
    {
        [JsonPropertyName("experiment_name")]
        public required string ExperimentName { get; init; }

        [JsonPropertyName("problem_path")]
        public required string ProblemPath { get; init; }

        [JsonPropertyName("model")]
        public required string Model { get; init; }

        [JsonPropertyName("repeat")]
        public required int Repeat { get; init; }

        [JsonPropertyName("param_dict")]
        public required Dictionary<string, string> ParamDict { get; init; }

        public override string ToString()
        {
            string msg = $"ExperimentName: {ExperimentName}\n";
            msg += $"ProblemPath: {ProblemPath}\n";
            msg += $"Model: {Model}\n";
            msg += $"Repeat: {Repeat}";
            return msg;
        }
    }

    /// <summary>
    /// コンフィグ情報を定義するレコード
    /// </summary>
    public record Configurations
    {
        [JsonPropertyName("url_base")]
        public required string UrlBase { get; init; }
    }
}

Interfaces

using System.Collections.Generic;
using System.Threading.Tasks;

namespace Common.Interfaces.MLflowInterface
{
    /// <summary>
    /// MLflowとの通信機能のインターフェース
    /// </summary>
    public interface IMLflowClient
    {
        /// <summary>
        /// Experiment, Runをスタートさせる
        /// </summary>
        /// <param name="runName"></param>
        /// <returns></returns>
        Task StartRunAsync(string runName);

        /// <summary>
        /// パラメータ名とパラメータを受け取り, MLflowに記録させる
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        Task LogParamAsync(string key, string value);

        /// <summary>
        /// パラメータ名とパラメータのディクショナリを受け取り, MLflowに記録させる
        /// </summary>
        /// <param name="paramDict"></param>
        /// <returns></returns>
        Task LogParamsAsync(Dictionary<string, string> paramDict);

        /// <summary>
        /// メトリクス名とメトリクスを受け取り, MLflowに記録させる
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="epoch"></param>
        /// <returns></returns>
        Task LogMetricAsync(string key, double value, int epoch = 0);

        /// <summary>
        /// ファイル名, ファイル内容を受け取り, Artifactにアップロードする
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="content"></param>
        /// <returns></returns>
        Task UploadArtifactAsync(string fileName, string content);

        /// <summary>
        /// ファイルパスを受け取り, Artifactにアップロードする
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        Task UploadArtifactAsync(string filePath);

        /// <summary>
        /// Runを終了させる
        /// </summary>
        /// <param name="status"></param>
        /// <returns></returns>
        Task TerminateRunAsync(string status = "FINISHED");
    }

    /// <summary>
    /// MLflowとの通信機能のインターフェースファクトリー
    /// </summary>
    public interface IMLflowClientFactory
    {
        /// <summary>
        /// クライアントを生成・取得する
        /// </summary>
        /// <param name="projectName"></param>
        /// <param name="experimentName"></param>
        /// <param name="urlBase"></param>
        /// <returns></returns>
        Task<IMLflowClient> CreateClient(string projectName, string experimentName, string urlBase);
    }
}
using Common.DataModels.CalculatorModels;
using Common.DataModels.Reporter;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Common.Interfaces.Solver
{
    /// <summary>
    /// ソルバのインターフェース
    /// </summary>
    public interface ISolver
    {
        /// <summary>
        /// パラメータをセットする
        /// </summary>
        /// <param name="paramDict"></param>
        public void SetParamDict(Dictionary<string, string> paramDict);

        /// <summary>
        /// 問題をセットする
        /// </summary>
        /// <param name="problem"></param>
        public void SetProblem(ProblemModel problem);

        /// <summary>
        /// 求解する
        /// </summary>
        /// <param name="progress"></param>
        /// <returns></returns>
        public Task<CalculationResult> SolveAsync(IProgress<ProgressReport> progress);
    }
}
using System.Threading.Tasks;

namespace Common.Interfaces.SystemMonitorInterface
{
    /// <summary>
    /// システムモニタリング用のインターフェース
    /// </summary>
    public abstract class AbstractSystemMonitor
    {
        protected IMLflowClient? _client;

        public void SetClient(IMLflowClient client)
        {
            _client = client;
        }

        /// <summary>
        /// バックグラウンドでの監視を開始する
        /// </summary>
        public abstract void Start();

        protected async Task LogMetricAsync(string key, double value, int step)
        {
            if (_client == null)
            {
                await Task.Delay(10);
            }
            else
            {
                await _client.LogMetricAsync(key, value, step);
            }
        }

        /// <summary>
        /// バックグラウンドでの監視を停止する
        /// </summary>
        /// <returns></returns>
        public abstract Task StopAsync();
    }
}

ExternalInterfacesプロジェクトの実装

MLflow

using Common.Interfaces.MLflowInterface;
using ExternalInterfaces.MLflow.Subs;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

namespace ExternalInterfaces.MLflow
{
    /// <summary>
    /// クライアントのファクトリー
    /// </summary>
    public class ClientFactory : IMLflowClientFactory
    {
        #region フィルド
        private readonly HttpClient _httpClient = CreateHttpClient();

        /// <summary>
        /// Project Name, URL, IMLflowClient
        /// </summary>
        private Dictionary<string, Dictionary<string, IMLflowClient>> _mlflowClientDict;
        #endregion フィルド

        #region コンストラクタ
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public ClientFactory()
        {
            _mlflowClientDict = [];
        }
        #endregion コンストラクタ

        #region メソッド
        /// <summary>
        /// クライアントを生成・取得する
        /// </summary>
        public async Task<IMLflowClient> CreateClient(string projectName, string experimentName, string urlBase)
        {
            IMLflowClient client;
            Dictionary<string, IMLflowClient> urlDict;
            if (_mlflowClientDict.ContainsKey(projectName))
            {
                urlDict = _mlflowClientDict[projectName];
            }
            else
            {
                urlDict = [];
                _mlflowClientDict[projectName] = urlDict;
            }

            if (urlDict.TryGetValue(urlBase, out IMLflowClient? value))
            {
                client = value;
            }
            else
            {
                client = await TryConnectAsync(projectName, experimentName, urlBase);
            }
            return client;
        }

        /// <summary>
        /// 静的クライアントを作成するためのヘルパーメソッド
        /// </summary>
        /// <returns></returns>
        private static HttpClient CreateHttpClient()
        {
            var handler = new HttpClientHandler()
            {
                UseProxy = false,
                Proxy = null
            };
            var client = new HttpClient(handler);
            client.DefaultRequestHeaders.ExpectContinue = false;
            client.DefaultRequestVersion = new Version(1, 1);

            // MLflowのようなHTTP/1.1のAPIに対しては, Keep-Aliveを促進するために
            // 接続アイドルタイムアウトを設定することも検討できるが
            // .NETの最新バージョンでは自動的にソケット再利用が行われることが多い
            return client;
        }

        /// <summary>
        /// 接続テストを行い, 適切なクライアントを返す
        /// </summary>
        private async Task<IMLflowClient> TryConnectAsync(string projectName, string experimentName, string urlBase)
        {
            // ヘルスチェック用エンドポイント
            var healthCheckUri = $"{urlBase}/";

            try
            {
                // 実際には認証込みでAPIを叩く
                var response = await _httpClient.GetAsync(healthCheckUri);

                if (response.IsSuccessStatusCode)
                {
                    // 接続成功: RealClientを返す
                    // RealClientのコンストラクタでURIとHttpClientを渡す想定
                    return new RealClient(_httpClient, projectName, experimentName, urlBase);
                }
                else
                {
                    // 接続失敗(4xx, 5xxなど): DummyClientを返す
                    Console.WriteLine($"[WARNING] MLflow connection failed with status: {response.StatusCode}. Using DummyClient.");
                    return new DummyClient(_httpClient, projectName, experimentName, urlBase);
                }
            }
            catch (HttpRequestException ex)
            {
                // 通信エラー(ネットワーク障害, DNS解決失敗など): DummyClientを返す
                Console.WriteLine($"[ERROR] Network error connecting to MLflow: {ex.Message}. Using DummyClient.");
                return new DummyClient(_httpClient, projectName, experimentName, urlBase);
            }
        }
        #endregion メソッド
    }
}
using Common.Interfaces.MLflowInterface;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

namespace ExternalInterfaces.MLflow.Subs
{
#pragma warning disable CS9113
    /// <summary>
    /// ダミーのMLflowインターフェース. 実際には通信を行わない.
    /// </summary>
    public class DummyClient(HttpClient httpClient, string projectName, string experimentName, string urlBase) : IMLflowClient
    {
#pragma warning restore CS9113
        #region メソッド
        /// <summary>
        /// 実験, Runをスタートする
        /// </summary>
        /// <param name="experimentName">実験名</param>
        /// <param name="runName">Run名</param>
        /// <returns></returns>
        public async Task StartRunAsync(string runName)
        {
            await Task.Delay(10);
        }

        /// <summary>
        /// keyとvalueを受け取り, パラメータとしてMLflowに記録する
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public async Task LogParamAsync(string key, string value)
        {
            await Task.Delay(10);
        }

        /// <summary>
        /// ディクショナリを受け取り, MLflowに記録する
        /// </summary>
        /// <param name="paramDict"></param>
        /// <returns></returns>
        public async Task LogParamsAsync(Dictionary<string, string> paramDict)
        {
            await Task.Delay(10);
        }

        /// <summary>
        /// ディクショナリを受け取り, MLflowに記録する
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="epoch"></param>
        /// <returns></returns>
        public async Task LogMetricAsync(string key, double value, int epoch = 0)
        {
            await Task.Delay(10);
        }

        /// <summary>
        /// 指定されたファイル名と内容でArtifactをアップロードする
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="content"></param>
        /// <returns></returns>
        public async Task UploadArtifactAsync(string fileName, string content)
        {
            await Task.Delay(10);
        }

        /// <summary>
        /// 指定されたファイルパスからArtifactにアップロードする
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public async Task UploadArtifactAsync(string filePath)
        {
            await Task.Delay(10);
        }

        /// <summary>
        /// statusの文字列を受け取り, Runを終わらせる
        /// </summary>
        /// <param name="status"></param>
        /// <returns></returns>
        public async Task TerminateRunAsync(string status = "FINISHED")
        {
            await Task.Delay(10);
        }
        #endregion メソッド
    }
}
using Common.DataModels.External;
using Common.Interfaces.MLflowInterface;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks;

namespace ExternalInterfaces.MLflow.Subs
{
    /// <summary>
    /// MLflowとの通信を行うインターフェース
    /// </summary>
    public class RealClient : IMLflowClient
    {
        #region フィルド
        /// <summary>
        /// HTTPのクライアント
        /// </summary>
        private readonly HttpClient _httpClient;

        /// <summary>
        /// URLのベース
        /// </summary>
        private readonly string _urlBase;

        /// <summary>
        /// 実験, Runのスタート用URL
        /// </summary>
        private readonly string _urlStart;

        /// <summary>
        /// RunsまでのURL
        /// </summary>
        private readonly string _urlRunsBase;

        /// <summary>
        /// RunのId
        /// </summary>
        private string _runId;
        #endregion フィルド

        #region コンストラクタ
        public RealClient(HttpClient httpClient, string projectName, string experimentName, string urlBase = "http://127.0.0.1:8000")
        {
            _httpClient = httpClient;
            _urlBase = urlBase;
            _urlStart = $"{_urlBase}/api/v1/projects/{projectName}/experiments/{experimentName}/start";
            _urlRunsBase = $"{_urlBase}/api/v1/projects/{projectName}/experiments/{experimentName}/runs";
            _runId = "";
        }
        #endregion コンストラクタ

        #region メソッド

        /// <summary>
        /// 実験, Runをスタートする
        /// </summary>
        /// <param name="experimentName">実験名</param>
        /// <param name="runName">Run名</param>
        /// <returns></returns>
        public async Task StartRunAsync(string runName)
        {
            // 実験スタートのリクエスト
            ExperimentRequest request = new()
            {
                RunName = runName,
            };
            // リクエストを送り, ステータスコードをチェックする
            HttpResponseMessage response = await _httpClient.PostAsJsonAsync(_urlStart, request);
            response.EnsureSuccessStatusCode();

            // Runを開始するリクエストを送る
            ExperimentResponse? startResponse = await response.Content.ReadFromJsonAsync<ExperimentResponse>();
            // Nullチェック
            if (startResponse is null)
            {
                throw new Exception("Failed to start run");
            }
            // Run Idをセットする
            _runId = startResponse.RunId;
        }

        /// <summary>
        /// keyとvalueを受け取り, パラメータとしてMLflowに記録する
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public async Task LogParamAsync(string key, string value)
        {
            // URLをRun Idも入れて策定する
            string urlLog = $"{_urlRunsBase}/{_runId}/log/params";
            // リクエストを生成する
            LogParamsRequest request = new()
            {
                ParamList =
                [
                    new() {Key=key, Value=value}
                ]
            };
            // レスポンスを受け取り, 内容をチェックする
            var response = await _httpClient.PostAsJsonAsync(urlLog, request);
            response.EnsureSuccessStatusCode();
        }

        /// <summary>
        /// ディクショナリを受け取り, MLflowに記録する
        /// </summary>
        /// <param name="paramDict"></param>
        /// <returns></returns>
        public async Task LogParamsAsync(Dictionary<string, string> paramDict)
        {
            // URLをRun Idも入れて策定する
            string urlLog = $"{_urlRunsBase}/{_runId}/log/params";
            // リクエストを生成する
            List<ParamPair> paramList = [];
            foreach (KeyValuePair<string, string> kv in paramDict)
            {
                ParamPair pair = new()
                {
                    Key = kv.Key,
                    Value = kv.Value
                };
                paramList.Add(pair);
            }
            LogParamsRequest request = new()
            {
                ParamList = paramList
            };
            // レスポンスを受け取り, 内容をチェックする
            var response = await _httpClient.PostAsJsonAsync(urlLog, request);
            response.EnsureSuccessStatusCode();
        }

        /// <summary>
        /// ディクショナリを受け取り, MLflowに記録する
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="epoch"></param>
        /// <returns></returns>
        public async Task LogMetricAsync(string key, double value, int epoch = 0)
        {
            // URLをRun Idも入れて策定する
            string urlLog = $"{_urlRunsBase}/{_runId}/log/metrics";
            // リクエストを生成する
            LogMetricsRequest request = new()
            {
                MetricList =
                [
                    new() {Key=key, Value=value, Epoch=epoch}
                ]
            };
            // レスポンスを受け取り, 内容をチェックする
            var response = await _httpClient.PostAsJsonAsync(urlLog, request);
            response.EnsureSuccessStatusCode();
        }

        /// <summary>
        /// 指定されたファイル名と内容でArtifactをアップロードする
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="content"></param>
        /// <returns></returns>
        public async Task UploadArtifactAsync(string fileName, string content)
        {
            // Run IDを含むアーティファクトアップロード用のURLを構築する
            string urlArtifact = $"{_urlRunsBase}/{_runId}/artifacts";

            // 文字列のコンテンツをUTF-8エンコーディングでバイト配列に変換する
            var fileContentBytes = Encoding.UTF8.GetBytes(content);
            using var byteArrayContent = new ByteArrayContent(fileContentBytes);

            // Content-Typeを汎用的なバイナリデータを示す "application/octet-stream" に設定する
            byteArrayContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

            // multipart/form-data 形式のコンテンツを作成する
            using var multipartContent = new MultipartFormDataContent
            {
                { byteArrayContent, "file", fileName }
            };

            // POSTリクエストを送信する
            var response = await _httpClient.PostAsync(urlArtifact, multipartContent);

            // レスポンスが成功ステータスコードでない場合に例外をスローする
            response.EnsureSuccessStatusCode();
        }

        /// <summary>
        /// 指定されたファイルパスからArtifactにアップロードする
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public async Task UploadArtifactAsync(string filePath)
        {
            if (string.IsNullOrEmpty(_runId))
            {
                throw new InvalidOperationException("Run has not been started. Call StartRunAsync first.");
            }

            // ファイルが存在するか確認
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException($"File not found at path: {filePath}");
            }

            // Run IDを含むアーティファクトアップロード用のURLを構築する
            string urlArtifact = $"{_urlRunsBase}/{_runId}/artifacts";

            // ファイル名 (パスの末尾) を取得
            string fileName = Path.GetFileName(filePath);

            // ファイルを非同期で読み込み、ストリームコンテンツとして使用する
            // using を使用してストリームが確実に閉じられるようにする
            using FileStream fileStream = new(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
            using StreamContent streamContent = new(fileStream);

            // Content-Typeを汎用的なバイナリデータを示す "application/octet-stream" に設定する
            streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

            // multipart/form-data 形式のコンテンツを作成する
            using var multipartContent = new MultipartFormDataContent
            {
                // ストリームコンテンツとファイル名を設定
                { streamContent, "file", fileName }
            };

            // POSTリクエストを送信する
            var response = await _httpClient.PostAsync(urlArtifact, multipartContent);

            // レスポンスが成功ステータスコードでない場合に例外をスローする
            response.EnsureSuccessStatusCode();
        }

        /// <summary>
        /// statusの文字列を受け取り, Runを終わらせる
        /// </summary>
        /// <param name="status"></param>
        /// <returns></returns>
        public async Task TerminateRunAsync(string status = "FINISHED")
        {
            string urlTerminate = $"{_urlRunsBase}/{_runId}/terminate";
            var request = new TerminateRunRequest { Status = status };
            var response = await _httpClient.PutAsJsonAsync(urlTerminate, request);
            response.EnsureSuccessStatusCode();
        }
        #endregion メソッド
    }
}

SystemMonitor

using Common.Interfaces.MLflowInterface;
using Common.Interfaces.SystemMonitorInterface;
using ExternalInterfaces.SystemMonitor.Subs;
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace ExternalInterfaces.SystemMonitor
{
    public static class MonitorFactory
    {
        /// <summary>
        /// 実行OSに基づき, 適切なシステムメトリクスモニターを生成する
        /// </summary>
        /// <param name="client">MLflowクライアントインスタンス</param>
        /// <param name="freqTime">メトリクス取得間隔 (ms)</param>
        /// <returns>ISystemMetricsMonitorの実装インスタンス</returns>
        public static AbstractSystemMonitor CreateMonitor(IMLflowClient client, int freqTime = 100)
        {
            AbstractSystemMonitor monitor;
            string key = "MonitoringTargetOS";
            string osName = GetOsName();
            // OSを判別
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                // Windowsの場合, PerformanceCounterを使用するMonitorを生成
                // SupportedOSPlatform属性が付いているため, ここでは警告を抑制する
#pragma warning disable CA1416
                Console.WriteLine("Creating WindowsSystemMonitor (PerformanceCounter).");
                monitor = new WindowsMonitor(freqTime);
#pragma warning restore CA1416
            }
            else
            {
                // Linux, macOSなどの場合, Processクラスを使用するMonitorを生成
                Console.WriteLine("Creating CrossSystemMonitor (Process class).");
                monitor = new CrossPlatformMonitor(freqTime);
            }

            // モニタにクライアントをセットする
            monitor.SetClient(client);

            // OS情報をMLflowにパラメータとして記録
            // ロギングのエラーを捕捉せず, Monitor生成処理をブロックしないようにTask.Runを使用
            Task.Run(async () =>
            {
                try
                {
                    await client.LogParamAsync(key, osName);
                }
                catch (Exception ex)
                {
                    // ログ処理のエラーはコンソールに出力するのみで, メイン処理は続行
                    Console.WriteLine($"Warning: Failed to log OS parameter to MLflow: {ex.Message}");
                }
            });

            return monitor;
        }

        private static string GetOsName()
        {
            string osName;
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                osName = "Windows";
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                osName = "Linux (Cross-Platform)";
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                osName = "macOS (Cross-Platform)";
            }
            else
            {
                osName = "Other Unix/Cross-Platform";
            }

            return osName;
        }
    }
}
using Common.Interfaces.SystemMonitorInterface;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ExternalInterfaces.SystemMonitor.Subs
{
    internal class CrossPlatformMonitor : AbstractSystemMonitor
    {
        #region フィルド
        private readonly int _freqTime;
        private readonly CancellationTokenSource _cts;
        private Task? _monitoringTask;
        private readonly Process _currentProcess;
        private TimeSpan _prevCpuTime;
        private DateTime _prevTime;
        #endregion フィルド

        #region コンストラクタ
        public CrossPlatformMonitor(int freqTime = 100)
        {
            _freqTime = freqTime;
            _cts = new CancellationTokenSource();

            // 現在のプロセスを取得
            _currentProcess = Process.GetCurrentProcess();

            // 初期値設定: 最初のCPU時間と時刻を記録
            _prevCpuTime = _currentProcess.TotalProcessorTime;
            _prevTime = DateTime.UtcNow;
        }
        #endregion コンストラクタ

        #region メソッド
        /// <summary>
        /// 監視をスタートする
        /// </summary>
        public override void Start()
        {
            _monitoringTask = Task.Run(() => MonitorLoopAsync(_cts.Token));
        }

        /// <summary>
        /// 監視を完了する
        /// </summary>
        /// <returns></returns>
        public override async Task StopAsync()
        {
            if (_monitoringTask is not null)
            {
                // キャンセル処理
                _cts.Cancel();
                // キャンセル処理完了を待つ
                await _monitoringTask;
            }

            // Process.GetCurrentProcess() はDispose不要
            _cts.Dispose();
            Console.WriteLine("System monitoring is stopped");
        }

        /// <summary>
        /// 監視を続けるループ処理
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        private async Task MonitorLoopAsync(CancellationToken token)
        {
            int step = 0;
            while (!token.IsCancellationRequested)
            {
                try
                {
                    // クラスベースに変更
                    double cpuUsage = CalculateCpuUsage();
                    // WorkingSet64 はプロセスが使用中の物理メモリ量 (バイト)
                    double memUsage = (double)(_currentProcess.WorkingSet64 / (1024.0 * 1024.0)); // MB 単位に変換

                    // 記録
                    await LogMetricAsync("system/process_cpu_utilization_percentage", cpuUsage, step);
                    await LogMetricAsync("system/process_working_set_mb", memUsage, step);

                    // ステップ更新&待ち時間
                    step++;
                    await Task.Delay(_freqTime, token);
                }
                catch (TaskCanceledException)
                {
                    break;
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Error logging system: {ex.Message}");
                }
            }
        }

        /// <summary>
        /// 実行中のプロセスが消費したCPU使用率を計算する(クロスプラットフォーム対応)
        /// </summary>
        /// <returns>CPU使用率 (%)</returns>
        private double CalculateCpuUsage()
        {
            // 現在のCPU時間と時刻を取得
            TimeSpan currentCpuTime = _currentProcess.TotalProcessorTime;
            DateTime currentTime = DateTime.UtcNow;

            // 経過したCPU時間(プロセスが使ったCPU時間)と、実際の経過時間を計算
            double cpuUsedMs = (currentCpuTime - _prevCpuTime).TotalMilliseconds;
            double totalTimeMs = (currentTime - _prevTime).TotalMilliseconds * Environment.ProcessorCount;

            // 次の計算のために値を更新
            _prevCpuTime = currentCpuTime;
            _prevTime = currentTime;

            if (totalTimeMs > 0)
            {
                // CPU 使用率 (%) = (プロセスが使ったCPU時間 / 経過時間 * 論理コア数) * 100
                // この値はシステム全体ではなく, このプロセスがCPUリソース全体に対して使った割合を示す
                return (double)(cpuUsedMs / totalTimeMs * 100.0);
            }
            return 0.0f;
        }
        #endregion メソッド
    }
}
using Common.Interfaces.SystemMonitorInterface;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace ExternalInterfaces.SystemMonitor.Subs
{
    internal class WindowsMonitor : AbstractSystemMonitor
    {
        #region フィルド
        private readonly int _freqTime;
        private readonly CancellationTokenSource _cts;
        private Task? _monitoringTask;
#pragma warning disable CA1416
        private PerformanceCounter _cpuCounter;
        private PerformanceCounter _memCounter;
#pragma warning restore CA1416
        #endregion フィルド

        #region コンストラクタ
        public WindowsMonitor(int freqTime = 100)
        {
            _freqTime = freqTime;
            _cts = new CancellationTokenSource();
#pragma warning disable CA1416
            _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
            _memCounter = new PerformanceCounter("Memory", "Available MBytes");
#pragma warning restore CA1416
        }
        #endregion コンストラクタ

        #region メソッド
        /// <summary>
        /// 監視をスタートする
        /// </summary>
        public override void Start()
        {
            _monitoringTask = Task.Run(() => MonitorLoopAsync(_cts.Token));
        }

        /// <summary>
        /// 監視を完了する
        /// </summary>
        /// <returns></returns>
        public override async Task StopAsync()
        {
            if (_monitoringTask is not null)
            {
                // キャンセル処理
                _cts.Cancel();
                // キャンセル処理完了を待つ
                await _monitoringTask;
            }

            _cpuCounter.Dispose();
            _memCounter.Dispose();
            _cts.Dispose();
            Console.WriteLine("System monitoring is stopped");
        }

        /// <summary>
        /// 監視を続けるループ処理
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        private async Task MonitorLoopAsync(CancellationToken token)
        {
            // 最初のCPU使用率を取得し, 1秒待つ
#pragma warning disable CA1416
            _cpuCounter.NextValue();
#pragma warning restore CA1416
            await Task.Delay(1000, token);

            int step = 0;
            while (!token.IsCancellationRequested)
            {
                try
                {
#pragma warning disable CA1416
                    double cpuUsage = _cpuCounter.NextValue();
                    double memUsage = _memCounter.NextValue();
#pragma warning restore CA1416
                    // 記録
                    await LogMetricAsync("system/cpu_utilization_percentage", cpuUsage, step);
                    await LogMetricAsync("system/available_memory_mb", memUsage, step);

                    // ステップ更新&待ち時間
                    step++;
                    await Task.Delay(_freqTime, token);
                }
                catch (TaskCanceledException)
                {
                    // 待機中にキャンセルされたらループを抜ける
                    break;
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Error logging system: {ex.Message}");
                }
            }
        }
        #endregion メソッド
    }
}

Application プロジェクトの実装

using Processor;
using System;

namespace Application
{
    public static class Prgram
    {
        static void Main(string[] args)
        {
            string settingsPath = args.Length > 0 ? args[0] : $"Data/Settings/settings.json";
            try
            {
                ProcessorComponent.MainAsync(settingsPath).GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                // 設定ファイルの読み込みエラーなど, MainAsync外の致命的なエラーを捕捉
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"\n[FATAL ERROR] Application failed to start or complete all runs: {ex.Message}");
                Console.ResetColor();
                Console.WriteLine($"Details: {ex}");
            }
        }
    }
}

Processor プロジェクトの実装

using Common.DataModels.CalculatorModels;
using Common.DataModels.Reporter;
using Common.DataModels.Settings;
using Common.Interfaces.MLflowInterface;
using Common.Interfaces.Solver;
using Common.Interfaces.SystemMonitorInterface;
using ExternalInterfaces.MLflow;
using ExternalInterfaces.SystemMonitor;
using Processor.ProcessorUnits;
using Processor.Utils;
using ShellProgressBar;
using Solver;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Processor
{
    public static class ProcessorComponent
    {
        public static async Task MainAsync(string settingsPath)
        {
            // 設定ファイル取得
            RootSettings settings = await JsonHandler.LoadSettingsAsync(settingsPath);

            // 固定情報取得
            FixedSettings fixedSettings = settings.Fixed;
            string projectName = fixedSettings.ProjectName;
            string configPath = fixedSettings.ConfigPath;
            string outDir = fixedSettings.OutDir;

            // コンフィグ情報取得
            Configurations configs = await JsonHandler.LoadConfigsAsync(configPath);
            string urlBase = configs.UrlBase;

            // ディレクトリ作成
            _ = PathHandler.CreateDir(outDir);

            // MLflowとのクライアントファクトリー
            ClientFactory clientFactory = new();

            // 問題のローディング処理インスタンス
            ProblemLoader problemLoader = new();

            // プログレスバーのオプション
            var options = new ProgressBarOptions
            {
                ForegroundColor = ConsoleColor.Yellow,
                BackgroundColor = ConsoleColor.DarkGray,
                ProgressCharacter = '-',
            };

            foreach (ExperimentSettings expSettings in settings.Experiments)
            {
                Console.WriteLine("====================================================================");
                Console.WriteLine(expSettings.ToString());
                // 実験設定取得
                string experimentName = expSettings.ExperimentName;
                string problemPath = expSettings.ProblemPath;
                string __model = expSettings.Model;
                int repeat = expSettings.Repeat;
                Dictionary<string, string> paramDict = expSettings.ParamDict;

                // MLflowとのクライアント作成
                IMLflowClient client = await clientFactory.CreateClient(projectName, experimentName, urlBase);

                // 問題の文章を取得する
                ProblemModel problem = problemLoader.GetProblem(problemPath);

                // モデル種別を取得する
                AlgorithmModels algorithmModel = ModelTypeGenerator.GetAlgorithmModel(__model);

                // Runの名前リスト作成
                List<string> runNameList = RunNameGenerator.GenerateRunNames(repeat);

                // イテレーション回数を取得する
                // ※進捗状況確認のため, イテレーション回数が必要
                // ※モデルによってイテレーション回数の表現が違うため, 関数で吸収
                int totalIterations = GetTotalIterations(algorithmModel, paramDict);
                Console.WriteLine("Start:");
                Console.WriteLine("-------------------------------------------------------------------");

                foreach (string runName in runNameList)
                {
                    // Runごとにソルバ作成
                    ISolver solver = SolverFactory.GetSolver(algorithmModel, client);
                    solver.SetParamDict(paramDict);
                    solver.SetProblem(problem);

                    // Runごとにシステムモニタ作成
                    ISystemMonitor monitor = MonitorFactory.CreateMonitor(client);
                    // Run開始
                    await client.StartRunAsync(runName);

                    try
                    {
                        // モニタリング開始
                        monitor.Start();

                        // 計算開始
                        CalculationResult result;
                        using (var pbar = new ProgressBar(totalIterations, "Calculating", options))
                        {
                            var progress = new Progress<ProgressReport>(report =>
                            {
                                var stepMessage = $"Step {report.CurrentStep}/{report.TotalSteps}, {report.Message}";
                                pbar.Tick(report.CurrentStep, stepMessage);
                            });

                            result = await solver.SolveAsync(progress);
                        }

                        // 後処理格納用のディレクトリを生成する
                        string runDir = $"{outDir}/{experimentName}/{runName}";
                        PathHandler.CreateDir(runDir);
                        // 計算結果を後処理
                        await PostProcessor.PostProcess(runDir, result, client);
                        await client.TerminateRunAsync();
                    }
                    catch (Exception ex)
                    {
                        // 何かあっても正常に終了させる
                        Console.WriteLine($"Solver exception failed: {ex.Message}");
                        await client.TerminateRunAsync("FAILED");
                    }
                    finally
                    {
                        // モニタリングを終了させ, インスタンスごと破棄する
                        await monitor.StopAsync();
                    }
                }
            }
        }

        /// <summary>
        /// モデルによってイテレーション回数の表現が違うため, 関数で取得する
        /// </summary>
        /// <param name="algorithmModel"></param>
        /// <param name="paramDict"></param>
        /// <returns></returns>
        private static int GetTotalIterations(AlgorithmModels algorithmModel, Dictionary<string, string> paramDict)
        {
            string iterationKey = algorithmModel switch
            {
                AlgorithmModels.AnnealingModel => "max_iterations",
                AlgorithmModels.GeneticAlgorithm => "generations",
                AlgorithmModels.IslandModel => "generations",
                _ => throw new ArgumentException($"Invalid type: {algorithmModel}")
            };

            _ = paramDict.TryGetValue(iterationKey, out string? iterationVal);
            bool tryParse = int.TryParse(iterationVal, out int totalIterations);
            if (tryParse)
            {
                return totalIterations;
            }
            else
            {
                throw new ArgumentException("Invalid ParamDict");
            }
        }
    }
}

using Common.DataModels.CalculatorModels;
using Processor.Utils;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Processor.ProcessorUnits
{
    /// <summary>
    /// 問題のローディング処理を行うクラス
    /// </summary>
    internal class ProblemLoader
    {
        /// <summary>
        /// 問題のファイルパスvs問題のオブジェクト
        /// </summary>
        private Dictionary<string, ProblemModel> _problemDict;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        internal ProblemLoader()
        {
            _problemDict = [];
        }

        /// <summary>
        /// ファイルパスを受け取り, 問題のオブジェクトを返す
        /// </summary>
        /// <param name="problemPath"></param>
        /// <returns></returns>
        public ProblemModel GetProblem(string problemPath)
        {
            ProblemModel problem;
            if (_problemDict.TryGetValue(problemPath, out ProblemModel? value))
            {
                problem = value;
            }
            else
            {
                var list = TextHandler.ReadText(problemPath);
                var coordinateDict = ArrangeProblem(list);
                problem = new ProblemModel
                {
                    BaseFile = problemPath,
                    Coordinates = coordinateDict
                };
                _problemDict[problemPath] = problem;
            }

            return problem;
        }

        /// <summary>
        /// 生のファイルデータを問題のオブジェクトに書き換える
        /// </summary>
        /// <param name="rawStrings"></param>
        /// <returns></returns>
        private Dictionary<int, Coordinate> ArrangeProblem(List<string> rawStrings)
        {
            Dictionary<int, Coordinate> coordinateDict = [];
            for (int i = 0; i < rawStrings.Count; i++)
            {
                string row = rawStrings[i];
                string[] splitedList = row.Split(' ');

                if (splitedList.Length == 3)
                {
                    bool parsable0 = int.TryParse(splitedList[0], out int cityIdx);
                    bool parsable1 = double.TryParse(splitedList[1], out double coordinateX);
                    bool parsable2 = double.TryParse(splitedList[2], out double coordinateY);

                    bool isOk = parsable0 && parsable1 && parsable2;
                    if (isOk)
                    {
                        coordinateDict[cityIdx] = new()
                        {
                            X = coordinateX,
                            Y = coordinateY
                        };
                    }
                    else
                    {
                        // 処理なし
                    }
                }
            }

            return coordinateDict;
        }
    }

    internal static class PostProcessor
    {
        internal static async Task PostProcess(string runDir, CalculationResult result, IMLflowClient client)
        {
            // まずはローカルにデータ保存
            string filePath = $"{runDir}/calc_result.json";
            await JsonHandler.SaveCalculationResultAsync(result, filePath);

            // Artifactsにアップロードする
            await client.UploadArtifactAsync(filePath);
        }
    }
}
using Common.DataModels.CalculatorModels;
using Common.DataModels.Settings;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace Processor.Utils
{
    internal static class JsonHandler
    {
        /// <summary>
        /// 指定されたパスのJSONファイルを非同期で読み込み, RootSettingオブジェクトにデシリアライズする
        /// </summary>
        /// <param name="filePath">JSON設定ファイルのパス</param>
        /// <returns>RootSettingオブジェクト</returns>
        /// <exception cref="FileNotFoundException">ファイルが見つからない場合にスローされる</exception>
        /// <exception cref="JsonException">JSONのデシリアライズに失敗した場合にスローされる</exception>
        public static async Task<RootSettings> LoadSettingsAsync(string filePath)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException($"Configuration file not found at path: {filePath}");
            }

            // 非同期でファイルを読み込む
            using FileStream fileStream = File.OpenRead(filePath);

            // JsonSerializerOptionsはデフォルトで十分だが, 必要に応じてカスタマイズ可能
            var options = new JsonSerializerOptions
            {
                // JSONで大文字・小文字が異なる場合のために設定
                PropertyNameCaseInsensitive = true,
                // コメントが含まれている場合のパースを許可
                ReadCommentHandling = JsonCommentHandling.Skip
            };

            // RootSettingにデシリアライズする
            RootSettings? settings = await JsonSerializer.DeserializeAsync<RootSettings>(fileStream, options);

            if (settings == null)
            {
                // ファイルが存在しても内容が空か不正な場合にスロー
                throw new JsonException($"Failed to deserialize configuration from file: {filePath}. Content might be invalid or empty.");
            }

            return settings;
        }

        public static async Task<Configurations> LoadConfigsAsync(string filePath)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException($"Configuration file not found at path: {filePath}");
            }

            // 非同期でファイルを読み込む
            using FileStream fileStream = File.OpenRead(filePath);

            // JsonSerializerOptionsはデフォルトで十分だが, 必要に応じてカスタマイズ可能
            var options = new JsonSerializerOptions
            {
                // JSONで大文字・小文字が異なる場合のために設定
                PropertyNameCaseInsensitive = true,
                // コメントが含まれている場合のパースを許可
                ReadCommentHandling = JsonCommentHandling.Skip
            };

            // Configurationsにデシリアライズする
            Configurations? configs = await JsonSerializer.DeserializeAsync<Configurations>(fileStream, options);

            if (configs == null)
            {
                // ファイルが存在しても内容が空か不正な場合にスロー
                throw new JsonException($"Failed to deserialize configuration from file: {filePath}. Content might be invalid or empty.");
            }

            return configs;
        }

        /// <summary>
        /// CalculationResultのインスタンスをJsonファイルに保存する
        /// </summary>
        /// <param name="result">保存するCalculationResultのインスタンス</param>
        /// <param name="filePath">保存先のファイルパス</param>
        public static async Task SaveCalculationResultAsync(CalculationResult result, string filePath)
        {
            // JsonSerializerOptionsを設定
            var options = new JsonSerializerOptions
            {
                // JSONを整形して読みやすくする(任意)
                WriteIndented = true,
                // 辞書のキーが数値(int)の場合でも、引用符を付けて文字列として扱う(推奨)
            };

            try
            {
                // モデルをJSON文字列にシリアライズ
                string jsonString = JsonSerializer.Serialize(result, options);

                // ファイルに非同期で書き込む
                await File.WriteAllTextAsync(filePath, jsonString);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"保存中にエラーが発生しました: {ex.Message}");
                Console.WriteLine($"スタックトレース: {ex.StackTrace}");
                throw;
            }
        }
    }

    internal static class ModelTypeGenerator
    {
        internal static AlgorithmModels GetAlgorithmModel(string model)
        {
            return model switch
            {
                "annealing" => AlgorithmModels.AnnealingModel,
                "genetic" => AlgorithmModels.GeneticAlgorithm,
                "island" => AlgorithmModels.IslandModel,
                _ => throw new Exception($"Invalid type: {model}")
            };
        }
    }

    /// <summary>
    /// Pathのハンドラ
    /// </summary>
    internal static class PathHandler
    {
        /// <summary>
        /// カレントディレクトリを取得する
        /// </summary>
        /// <returns></returns>
        public static string GetCurrentDir()
        {
            return Directory.GetCurrentDirectory();
        }

        /// <summary>
        /// ディレクトリを作成し, 作成したかどうかをbool型で返す
        /// </summary>
        /// <param name="dirPath"></param>
        /// <returns></returns>
        public static bool CreateDir(string dirPath)
        {
            bool created;
            if (Directory.Exists(dirPath))
            {
                created = false;
            }
            else
            {
                try
                {
                    Directory.CreateDirectory(dirPath);
                    created = true;
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    created = false;
                }
            }

            return created;
        }
    }

    internal static class RunNameGenerator
    {
        /// <summary>
        /// 繰り返し実行される実験のための、一貫性のあるRun名を生成する
        /// (例: 2025_1005_0807_01, 2025_1005_0807_02, ...)
        /// </summary>
        /// <param name="repeatCount">実験の繰り返し回数</param>
        /// <returns>生成されたRun名のリスト</returns>
        internal static List<string> GenerateRunNames(int repeatCount)
        {
            if (repeatCount <= 0)
            {
                return [];
            }

            // 現在時刻の取得とフォーマット(例: "2025_1005_0807")
            string now = DateTime.Now.ToString("yyyy_MMdd_HHmm");

            // 桁数の計算 (例: repeat=100 なら digits=3)
            int digits = repeatCount.ToString().Length;

            var runNames = new List<string>();

            for (int i = 0; i < repeatCount; i++)
            {
                // インデックスをゼロパディング (例: i=0 なら "01", i=99 なら "100")
                // Pythonのzfill(digits)に相当
                string runIndex = (i + 1).ToString().PadLeft(digits, '0');

                // Run名の結合 (例: "2025_1005_0807_01")
                string runName = $"{now}_{runIndex}";

                runNames.Add(runName);
            }

            return runNames;
        }
    }

    /// <summary>
    /// テキストファイルを扱う静的クラス
    /// </summary>
    internal static class TextHandler
    {
        /// <summary>
        /// ファイルパスを受け取り, テキストファイルを1行ずつリストにして返す
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        internal static List<string> ReadText(string filePath)
        {
            StreamReader sr = new(filePath, Encoding.GetEncoding("utf-8"));
            List<string> strings = [];
            while (sr.Peek() != -1)
            {
                string? str = sr.ReadLine();
                if (str == null)
                {

                }
                else
                {
                    strings.Add(str);
                }
            }

            sr.Close();

            return strings;
        }
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?