NI DAQハードウェア駆動、指数フィッティングとスプライン補間の追加、LiveCharts2によるUI最適化、パフォーマンス監視の実装

1. ハードウェアドライバの実装(NI DAQを例に)

ここでは、NI DAQmx APIを使用したIDeviceControllerIDataCollectorServiceの実装を示します。前提として、NI DAQmxドライバのインストール(NationalInstruments.DAQmx NuGetパッケージ)とデバイスの接続・設定が必要です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NationalInstruments.DAQmx;
using Microsoft.Extensions.Logging;
using PowerCycling.Domain;

namespace PowerCycling.Infrastructure
{
    public class NiDaqDeviceController : IDeviceController
    {
        private readonly ILogger<NiDaqDeviceController> _logger;
        private Task _controlTask;
        private bool _isRunning;

        public NiDaqDeviceController(ILogger<NiDaqDeviceController> logger) => _logger = logger;

        public async Task ConfigureAsync(ParamSetupModel parameters, CancellationToken ct)
        {
            try
            {
                _logger.LogInformation("NI DAQデバイス設定開始...");
                using var aoTask = new NationalInstruments.DAQmx.Task();
                aoTask.AOChannels.CreateVoltageChannel("Dev1/ao0", "Control", 0, 5, AOVoltageUnits.Volts);
                aoTask.Control(TaskAction.Commit);
                _logger.LogInformation($"出力チャンネル設定: 電流 {parameters.ProjectCurrent:F2} A");

                using var tempTask = new NationalInstruments.DAQmx.Task();
                tempTask.AOChannels.CreateVoltageChannel("Dev1/ao1", "Temp", 0, 10, AOVoltageUnits.Volts);
                tempTask.Control(TaskAction.Commit);
                _logger.LogInformation($"コールドプレート温度設定: {parameters.ProjectColdplatTemp:F2} ℃");
                await Task.CompletedTask;
            }
            catch (DaqException ex)
            {
                _logger.LogError(ex, "NI DAQ設定失敗");
                throw;
            }
        }

        public async Task StartAsync(CancellationToken ct)
        {
            _isRunning = true;
            _controlTask = Task.Run(async () =>
            {
                while (_isRunning && !ct.IsCancellationRequested)
                {
                    await Task.Delay(1000, ct);
                    _logger.LogInformation("NI DAQデバイス動作中...");
                }
            }, ct);
            await Task.CompletedTask;
        }

        public async Task PauseAsync(CancellationToken ct)
        {
            _isRunning = false;
            _logger.LogInformation("NI DAQ一時停止");
            await Task.CompletedTask;
        }

        public async Task StopAsync(CancellationToken ct)
        {
            _isRunning = false;
            _logger.LogInformation("NI DAQ停止");
            await Task.CompletedTask;
        }

        public async Task ResumeAsync(CancellationToken ct)
        {
            if (!_isRunning) await StartAsync(ct);
        }

        public async Task<bool> IsDeviceConnectedAsync(CancellationToken ct)
        {
            try
            {
                var connected = DaqSystem.Local.Devices.Any(d => d.Contains("Dev1"));
                _logger.LogInformation($"NI DAQ接続状態: {(connected ? "接続済み" : "未接続")}");
                return await Task.FromResult(connected);
            }
            catch (DaqException ex)
            {
                _logger.LogError(ex, "接続確認失敗");
                return false;
            }
        }

        public async Task<string> GetDeviceStatusAsync(CancellationToken ct) =>
            await Task.FromResult(_isRunning ? "Running" : "Stopped");
    }

    public class NiDaqDataCollectorService : IDataCollectorService
    {
        private readonly ILogger<NiDaqDataCollectorService> _logger;
        private NationalInstruments.DAQmx.Task _aiTask;

        public NiDaqDataCollectorService(ILogger<NiDaqDataCollectorService> logger) => _logger = logger;

        public async Task InitializeAsync(CancellationToken ct)
        {
            try
            {
                _logger.LogInformation("NI DAQデータ収集初期化...");
                _aiTask = new NationalInstruments.DAQmx.Task();
                _aiTask.AIChannels.CreateVoltageChannel("Dev1/ai0", "VCE", AIVoltageUnits.Volts, 0, 5, AIVoltageUnits.Volts);
                _aiTask.AIChannels.CreateThermocoupleChannel("Dev1/ai1", "TVJ", 0, 100, ThermocoupleType.K, AITemperatureUnits.DegreesC);
                _aiTask.Timing.ConfigureSampleClock("", 1000, SampleClockActiveEdge.Rising, SampleQuantityMode.ContinuousSamples);
                _aiTask.Control(TaskAction.Commit);
                _logger.LogInformation("初期化完了");
            }
            catch (DaqException ex)
            {
                _logger.LogError(ex, "初期化失敗");
                throw;
            }
            await Task.CompletedTask;
        }

        public async Task<Dictionary<string, List<double>>> CollectDataAsync(CancellationToken ct)
        {
            var data = new Dictionary<string, List<double>>
            {
                { "VCE", new List<double>() }, { "VCETime", new List<double>() },
                { "TVJ", new List<double>() }, { "TVJTime", new List<double>() }, { "RthJC", new List<double>() }
            };

            try
            {
                _logger.LogInformation("データ収集開始...");
                using var reader = new AnalogMultiChannelReader(_aiTask.Stream);
                double time = 0;
                for (int i = 0; i < 100 && !ct.IsCancellationRequested; i++)
                {
                    var samples = await Task.Run(() => reader.ReadSingleSample(), ct);
                    data["VCE"].Add(samples[0]);
                    data["VCETime"].Add(time);
                    data["TVJ"].Add(samples[1]);
                    data["TVJTime"].Add(time);
                    data["RthJC"].Add(CalcRth(samples[0], samples[1]));
                    time += 0.001;
                    await Task.Delay(1, ct);
                }
                _logger.LogInformation("データ収集完了");
            }
            catch (DaqException ex)
            {
                _logger.LogError(ex, "データ収集失敗");
                throw;
            }
            return data;
        }

        public async Task<double> GetCurrentValueAsync(string channel, CancellationToken ct)
        {
            try
            {
                using var task = new NationalInstruments.DAQmx.Task();
                if (channel == "VCE") task.AIChannels.CreateVoltageChannel("Dev1/ai0", channel, AIVoltageUnits.Volts, 0, 5, AIVoltageUnits.Volts);
                else if (channel == "TVJ") task.AIChannels.CreateThermocoupleChannel("Dev1/ai1", channel, 0, 100, ThermocoupleType.K, AITemperatureUnits.DegreesC);
                else return 0;
                using var reader = new AnalogSingleChannelReader(task.Stream);
                return await Task.Run(() => reader.ReadSingleSampleDouble(), ct);
            }
            catch (DaqException ex)
            {
                _logger.LogError(ex, $"{channel}読み取り失敗");
                return 0;
            }
        }

        private double CalcRth(double vce, double tvj) => tvj > 0 ? vce / tvj : 0;
    }
}

説明:

  • ConfigureAsyncで出力チャンネル(電流、温度制御)を設定
  • CollectDataAsyncで1ms間隔でアナログ入力(電圧、温度)を収集
  • GetCurrentValueAsyncで単一チャンネルのリアルタイム値を取得

2. フィッティングアルゴリズムの拡張

指数フィッティングとスプライン補間を追加し、UIから選択可能にします。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MathNet.Numerics;
using MathNet.Numerics.Interpolation;
using Microsoft.Extensions.Logging;
using PowerCycling.Domain;

namespace PowerCycling.Infrastructure
{
    public enum FitMethod { Polynomial, Exponential, Spline }

    public interface ICurveFittingService
    {
        Task<List<ChartValue>> ApplyFitAsync(List<ChartValue> data, FitMethod method, int degree, CancellationToken ct);
        Task<List<ChartValue>> PreprocessAsync(List<ChartValue> data, CancellationToken ct);
    }

    public class CurveFittingService : ICurveFittingService
    {
        private readonly ILogger<CurveFittingService> _logger;

        public CurveFittingService(ILogger<CurveFittingService> logger) => _logger = logger;

        public async Task<List<ChartValue>> ApplyFitAsync(List<ChartValue> data, FitMethod method, int degree, CancellationToken ct)
        {
            if (data == null || data.Count < 2) { _logger.LogWarning("データ不足"); return data; }

            return await Task.Run(() =>
            {
                try
                {
                    var xs = data.Select(v => v.X).ToArray();
                    var ys = data.Select(v => v.Y).ToArray();
                    var station = data.First().Station;
                    List<ChartValue> result;

                    switch (method)
                    {
                        case FitMethod.Polynomial:
                            var coeff = Fit.Polynomial(xs, ys, degree);
                            result = GenPoints(xs, coeff, station, "Poly");
                            _logger.LogInformation($"多項式フィット完了: {station}, 次数 {degree}");
                            break;
                        case FitMethod.Exponential:
                            var expCoeff = FitExponential(xs, ys);
                            result = GenPoints(xs, expCoeff, station, "Exp");
                            _logger.LogInformation($"指数フィット完了: {station}");
                            break;
                        case FitMethod.Spline:
                            var spline = CubicSpline.InterpolateNatural(xs, ys);
                            result = GenSplinePoints(xs, spline, station);
                            _logger.LogInformation($"スプライン補間完了: {station}");
                            break;
                        default: result = data; break;
                    }
                    return result;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "フィッティング失敗");
                    return data;
                }
            }, ct);
        }

        public async Task<List<ChartValue>> PreprocessAsync(List<ChartValue> data, CancellationToken ct)
        {
            if (data == null || data.Count == 0) { _logger.LogWarning("空データ"); return data; }

            return await Task.Run(() =>
            {
                int winSize = Math.Max(3, data.Count / 20);
                var xs = data.Select(v => v.X).ToArray();
                var ys = data.Select(v => v.Y).ToArray();
                var station = data.First().Station;

                var smoothed = new double[ys.Length];
                for (int i = 0; i < ys.Length; i++)
                {
                    int start = Math.Max(0, i - winSize / 2);
                    int end = Math.Min(ys.Length - 1, i + winSize / 2);
                    smoothed[i] = Enumerable.Range(start, end - start + 1).Average(j => ys[j]);
                }

                var res = new List<ChartValue>();
                for (int i = 0; i < xs.Length; i++)
                    res.Add(new ChartValue { X = xs[i], Y = smoothed[i], Station = station + "_Pre" });
                _logger.LogInformation($"前処理完了: {station}");
                return res;
            }, ct);
        }

        private List<ChartValue> GenPoints(double[] xs, double[] coeff, string station, string tag)
        {
            var points = new List<ChartValue>();
            double min = xs.Min(), max = xs.Max(), step = (max - min) / 99;
            for (int i = 0; i < 100; i++)
            {
                double x = min + i * step;
                double y = tag == "Exp" ? coeff[0] * Math.Exp(coeff[1] * x) : EvalPoly(x, coeff);
                points.Add(new ChartValue { X = x, Y = y, Station = $"{station}_{tag}" });
            }
            return points;
        }

        private List<ChartValue> GenSplinePoints(double[] xs, IInterpolation spline, string station)
        {
            var points = new List<ChartValue>();
            double min = xs.Min(), max = xs.Max(), step = (max - min) / 99;
            for (int i = 0; i < 100; i++)
            {
                double x = min + i * step;
                points.Add(new ChartValue { X = x, Y = spline.Interpolate(x), Station = $"{station}_Spline" });
            }
            return points;
        }

        private double[] FitExponential(double[] x, double[] y)
        {
            var logY = y.Select(v => Math.Log(v)).ToArray();
            var coeff = Fit.Polynomial(x, logY, 1);
            return new[] { Math.Exp(coeff[0]), coeff[1] };
        }

        private double EvalPoly(double x, double[] coeff) => coeff.Select((c, i) => c * Math.Pow(x, i)).Sum();
    }
}

ViewModel側の更新例:

// TestViewModel内
private FitMethod _selectedFit = FitMethod.Polynomial;
private int _polyDegree = 3;

public FitMethod SelectedFit { get => _selectedFit; set => SetProperty(ref _selectedFit, value); }
public int PolyDegree { get => _polyDegree; set => SetProperty(ref _polyDegree, value); }

private async Task FitCurveCmd(string station)
{
    var raw = SelectedProjectModel.ChartValueList.Where(v => v.Station == station).ToList();
    var fitted = await _fittingService.ApplyFitAsync(raw, SelectedFit, PolyDegree, _cts.Token);
    foreach (var v in fitted) SelectedProjectModel.ChartValueList.Add(v);
    Global.LogAndPop($"フィット完了: {station} - {SelectedFit}", Level.INFO);
}

XAML例:

<StackPanel Orientation="Horizontal">
    <TextBlock Text="フィット方式" />
    <ComboBox SelectedItem="{Binding SelectedFit}">
        <ComboBoxItem Content="多項式" />
        <ComboBoxItem Content="指数" />
        <ComboBoxItem Content="スプライン" />
    </ComboBox>
    <TextBlock Text="次数" Visibility="{Binding SelectedFit, Converter={StaticResource FitToVis}, ConverterParameter=Polynomial}" />
    <TextBox Text="{Binding PolyDegree}" Visibility="{Binding SelectedFit, Converter={StaticResource FitToVis}, ConverterParameter=Polynomial}" />
</StackPanel>

3. UI最適化(LiveCharts2)

LiveChartsCore.SkiaSharpView.WPF NuGetパッケージを導入し、既存のCanvas描画を置き換えます。

<UserControl ...>
    <Grid>
        <lvc:CartesianChart Series="{Binding Series}" ZoomMode="Both" TooltipPosition="Top"
                            XAxes="{Binding XAxis}" YAxes="{Binding YAxis}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseMove">
                    <i:InvokeCommandAction Command="{Binding ChartMoveCmd}" PassEventArgsToCommand="True"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </lvc:CartesianChart>
    </Grid>
</UserControl>

ViewModel側:

using LiveChartsCore; using LiveChartsCore.Defaults; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using SkiaSharp;

public class TestViewModel : BindableBase
{
    public ObservableCollection<ISeries> Series { get; set; } = new();
    public Axis[] XAxis { get; set; } = { new Axis { Name = "時間 (s)" } };
    public Axis[] YAxis { get; set; } = { new Axis { Name = "値" } };

    public DelegateCommand<MouseEventArgs> ChartMoveCmd { get; }

    public TestViewModel()
    {
        ChartMoveCmd = new DelegateCommand<MouseEventArgs>(OnChartMove);
        // データ更新時の処理
        _curveGen.CurveUpdated += chartValues =>
        {
            Series.Clear();
            foreach (var group in chartValues.GroupBy(v => v.Station))
            {
                Series.Add(new LineSeries<ObservablePoint>
                {
                    Name = group.Key,
                    Values = group.Select(v => new ObservablePoint(v.X, v.Y)).ToList(),
                    Stroke = new SolidColorPaint(GetColor(group.Key)) { StrokeThickness = 2 },
                    Fill = null
                });
            }
        };
    }

    private void OnChartMove(MouseEventArgs e)
    {
        var chart = e.Source as CartesianChart;
        if (chart == null) return;
        var pos = e.GetPosition(chart);
        var nearest = Series.SelectMany(s => s.Values.Cast<ObservablePoint>().Select(p => new { p, s.Name }))
                            .OrderBy(p => Math.Abs(p.p.X - pos.X)).FirstOrDefault();
        if (nearest != null) Global.LogAndPop($"[{nearest.Name}] X:{nearest.p.X:F2}, Y:{nearest.p.Y:F2}", Level.INFO);
    }

    private SKColor GetColor(string name) => name switch
    {
        string n when n.Contains("VCE") => SKColors.Blue,
        string n when n.Contains("TVJ") => SKColors.Red,
        _ => SKColors.Black
    };
}

4. パフォーマンス監視

System.Diagnosticsを使用してCPU使用率とメモリを監視します。

public interface IPerfMonitor
{
    Task StartAsync(CancellationToken ct);
    event Action<double, double> OnUpdate; // CPU%, メモリMB
}

public class PerfMonitor : IPerfMonitor
{
    private readonly ILogger<PerfMonitor> _logger;
    private readonly PerformanceCounter _cpu = new("Processor", "% Processor Time", "_Total");
    private readonly PerformanceCounter _mem = new("Memory", "Available MBytes");

    public event Action<double, double> OnUpdate;

    public PerfMonitor(ILogger<PerfMonitor> logger) => _logger = logger;

    public async Task StartAsync(CancellationToken ct)
    {
        await Task.Run(async () =>
        {
            while (!ct.IsCancellationRequested)
            {
                try
                {
                    var cpu = _cpu.NextValue();
                    var mem = _mem.NextValue();
                    OnUpdate?.Invoke(cpu, mem);
                    _logger.LogInformation($"CPU: {cpu:F2}%, 空きメモリ: {mem:F2} MB");
                    await Task.Delay(1000, ct);
                }
                catch (Exception ex) { _logger.LogError(ex, "監視エラー"); }
            }
        }, ct);
    }
}

ViewModelとの統合:

// TestViewModel内
private double _cpuUsage, _availMem;
public double CpuUsage { get => _cpuUsage; set => SetProperty(ref _cpuUsage, value); }
public double AvailMem { get => _availMem; set => SetProperty(ref _availMem, value); }

// コンストラクタで
var perf = serviceProvider.GetRequiredService<IPerfMonitor>();
perf.OnUpdate += (cpu, mem) => { CpuUsage = cpu; AvailMem = mem; };
_ = perf.StartAsync(_cts.Token);

XAML表示例:

<StackPanel Orientation="Horizontal">
    <TextBlock Text="CPU:" />
    <TextBlock Text="{Binding CpuUsage, StringFormat={}{0:F2}%}" />
    <TextBlock Text="空きメモリ:" />
    <TextBlock Text="{Binding AvailMem, StringFormat={}{0:F2} MB}" />
</StackPanel>

5. DIコンテナ設定

services.AddSingleton<IDeviceController, NiDaqDeviceController>();
services.AddSingleton<IDataCollectorService, NiDaqDataCollectorService>();
services.AddSingleton<ICurveFittingService, CurveFittingService>();
services.AddSingleton<IPerfMonitor, PerfMonitor>();

6. 利用上の注意

  • NI DAQデバイス(Dev1)を使用。チャンネル設定は環境に合わせて変更してください。
  • フィッティング方式は多項式(次数指定可)、指数、スプラインから選択可能。
  • LiveCharts2チャートは拡大・縮小・ドラッグに対応。マウスホバーで近傍点の座標を表示。
  • パフォーマンス監視はCPU使用率と空きメモリを1秒間隔で取得。閾値超過時に警告ログを出力。

タグ: NI-DAQ DAQmx CurveFitting ExponentialFit SplineInterpolation

5月30日 09:04 投稿