C# - PLplot 사용 예제
지난 글에서 설명한,
ML.NET Model Builder - 회귀(Regression), 다중 분류(Multi-class classification) 예제
; https://www.sysnet.pe.kr/2/0/11895
Regression_TaxiFarePrediction 예제를 보면,
dotnet/machinelearning-samples
; https://github.com/dotnet/machinelearning-samples/tree/master/samples/csharp/getting-started/Regression_TaxiFarePrediction
PLplot 라이브러리를 사용하는
코드와 출력 사례를 볼 수 있습니다.
이참에, ^^ 간단한 사용법을 정리해 보겠습니다.
PLplot 설치를 하고,
Install-Package PLplot
* 설치 후 csproj에 다음의 항목이 추가됩니다.
<PackageReference Include="PLplot" Version="5.13.7" />
간단한 실습으로 예전에 그냥 점(dot)으로만 그려봤던,
그래프 그리기로 알아보는 뉴턴-랩슨(Newton-Raphson's method)법과 제곱근 구하기 - C#
; https://www.sysnet.pe.kr/2/0/10911
2차 방정식의 곡선을 PLplot으로 그려 보면 다음과 같습니다.
static void Main(string[] args)
{
const int yMin = -30;
const int yMax = 100;
const int xMin = -20;
const int xMax = 20;
Func<double, double> quad = (x) => x * x - 10;
double [] xData = PythonUtils.Range(xMin, xMax, 0.1).ToArray();
double [] yData = (from item in xData
select quad(item)).ToArray();
using (var pl = new PLStream())
{
pl.sdev("png");
pl.sfnam("test.png");
pl.spal0("cmap0_alternate.pal");
pl.init();
pl.env(xMin, xMax, yMin, yMax, AxesScale.Independent, AxisBox.BoxTicksLabelsAxes);
pl.lab("x", "y = x#u2#d - 10", "Simple PLplot demo of a 2D line plot");
pl.line(xData, yData);
pl.gver(out var verText);
}
}
보는 바와 같이 x 좌표의 값과 그에 대응하는 인덱스에 y 좌표의 값을 담아 pl.line 함수에 전달하기만 하면 됩니다. 그럼, 다음과 같은 이미지를 담은 PNG 파일이 생성됩니다. ^^
이번엔 데이터의 분포도를 알 수 있도록 line이 아닌 점으로 표시해 보겠습니다. 데이터는 다음의 책에 있는,
기초 수학으로 이해하는 머신러닝 알고리즘
; https://wikibook.co.kr/math-for-ml/
click.csv 파일을 로드해서 pl.line만 pl.poin으로 바꿔주면,
Dictionary<string, List<double>> data = PythonUtils.Load<double>("click.csv");
List<double> xData = data["x"];
List<double> yData = data["y"];
double xMin = xData.Min() - 10;
double xMax = xData.Max() + 10;
double yMin = yData.Min() - 10;
double yMax = yData.Max() + 10;
string chartFileName = "click.svg";
using (var pl = new PLStream())
{
pl.sdev("svg");
pl.sfnam(chartFileName);
pl.spal0("cmap0_alternate.pal");
pl.init();
pl.env(xMin, xMax, yMin, yMax, AxesScale.Independent, AxisBox.BoxTicksLabelsAxes);
// Set scaling for mail title text 125% size of default
// pl.schr(0, 1.25);
// The main title
pl.lab("X", "Y", "Click");
//This code is the symbol to paint
// http://personalpages.to.infn.it/~zaninett/libri/hershey.html
char code = Symbol.Bullet; // == 17;
// plot using different colors
// see http://plplot.sourceforge.net/examples.php?demo=02 for palette indices
//pl.col0(9); //Light Green
//pl.col0(4); //Red
pl.col0(2); //Blue
pl.poin(xData.ToArray(), yData.ToArray(), code);
// end page (writes output to disk)
pl.eop();
// output version of PLplot
pl.gver(out var verText);
}
다음과 같은 출력의 svg 파일이 생성됩니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
간단하죠? ^^ 좀 더 알고 싶다면 온라인 도움말을 참고할 수 있습니다.
API Reference manual
; https://surban.github.io/PLplotNet/api/PLplot.PLStream.html
그렇긴 해도 역시나 레퍼런스 매뉴얼보다는, 다른 사람들이 작성한 예제 코드와 그것의 출력 결과를 통해 익히는 것이 가장 빠릅니다. ^^
참고로, 예전에 OxyPlot을 소개한 적이 있습니다.
C# Plotting 라이브러리 OxyPlot
; https://www.sysnet.pe.kr/2/0/10973
입맛에 맞는 걸로 쓰시면 되겠죠? ^^ 게다가 아예 언어를 바꾸는 것도 고려해볼만합니다. 예를 들어 이 글의 그래프를 그리는 코드를 R 언어로 바꾸면 다음과 같이 아주 간단하게 표현할 수 있습니다.
x = seq(from=-20, to=20, by=0.1)
y = x * x - 10
plot(x, y, col="gray", type="l") // 숫자 1이 아닌, 소문자 'l'
마지막으로 PLplot 사용 시 오류 발생 상황을 정리해 보겠습니다. "using (var pl = new PLStream())" 코드 수행 시 다음과 같은 예외가 발생한다면?
System.TypeInitializationException
HResult=0x80131534
Message=The type initializer for 'PLplot.Native' threw an exception.
Source=PLplotNet
StackTrace:
at PLplot.Native.mkstrm(Int32& p_strm)
at PLplot.PLStream..ctor() in C:\projects\plplotnet\PLplotNet\PLStream.cs:line 23
at ConsoleApp1.Program.Main(String[] args) in E:\ConsoleApp1\ConsoleApp1\Program.cs:line 21
Inner Exception 1:
InvalidOperationException: Cannot find support PLplot support files in System.String[].
이게 좀 원인이 복잡합니다. 일반적으로 NuGet 패키지를 PackageReference로 참조하면,
Visual Studio 2017 - NuGet 패키지를 직접 참조하는 PackageReference 지원
; https://www.sysnet.pe.kr/2/0/11281
솔루션 파일이 있는 폴더의 /packages 하위 폴더에 라이브러리가 구성되지 않고 "%USERPROFILE%\.nuget\packages\plplot\5.13.7\runtimes\win-x64\native" 폴더에 놓이게 됩니다. 여기서 문제는, native 구성 요소는 nuget cache 폴더에 있는 반면, managed 구성 요소인 "PLplotNet.dll" 파일이 프로젝트의 출력 폴더(/bin/Debug)에 놓인다는 점입니다. 이 때문에 PLplotNet.dll은 현재 디렉터리에서 네이티브 dll을 찾으려 하고 그것이 없어 저런 식으로 오류가 발생하는 것입니다.
따라서 간단하게 문제를 해결하려면 "%USERPROFILE%\.nuget\packages\plplot\5.13.7\runtimes\win-x64\native" 폴더의 모든 파일을 프로젝트의 출력 폴더에 복사해 놓는 것입니다. 단지 그러면 바이너리 폴더의 내용이 너무 많아 귀찮아지므로 privatePath를 이용해,
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath=".\plplot" />
</assemblyBinding>
</runtime>
</configuration>
다음과 같이 선처리를 해주면 됩니다.
using System;
using System.IO;
using System.Reflection;
class PLPlotEnv
{
const string ManagedDllFileName = @"PLplotNet.dll";
public static void SetEnv()
{
do
{
if (File.Exists(@".\plplot\plplot.dll") == true)
{
break;
}
if (File.Exists(ManagedDllFileName) == false)
{
break;
}
Version ver = AssemblyName.GetAssemblyName(ManagedDllFileName).Version;
string versionText = $"{ver.Major}.{ver.Minor}.{ver.Build}";
string path = Environment.ExpandEnvironmentVariables($@"%USERPROFILE%\.nuget\packages\plplot\{versionText}\runtimes\win-x64\native");
if (Directory.Exists(path) == false)
{
break;
}
string targetPath = Path.GetDirectoryName(typeof(PLPlotEnv).Assembly.Location);
targetPath = Path.Combine(targetPath, "plplot");
DirectoryCopy(path, targetPath, true);
} while (false);
CopyPLplotNetFile();
// Environment.SetEnvironmentVariable("PLPLOT_LIB", ...);
}
private static void CopyPLplotNetFile()
{
if (File.Exists(ManagedDllFileName) == false)
{
return;
}
if (File.Exists($@".\plplot\{ManagedDllFileName}") == false)
{
File.Copy(ManagedDllFileName, $@".\plplot\{ManagedDllFileName}");
}
File.Delete(ManagedDllFileName);
}
// https://learn.microsoft.com/ko-kr/dotnet/standard/io/how-to-copy-directories
private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs)
{
// Get the subdirectories for the specified directory.
DirectoryInfo dir = new DirectoryInfo(sourceDirName);
if (!dir.Exists)
{
throw new DirectoryNotFoundException(
"Source directory does not exist or could not be found: "
+ sourceDirName);
}
DirectoryInfo[] dirs = dir.GetDirectories();
// If the destination directory doesn't exist, create it.
if (!Directory.Exists(destDirName))
{
Directory.CreateDirectory(destDirName);
}
// Get the files in the directory and copy them to the new location.
FileInfo[] files = dir.GetFiles();
foreach (FileInfo file in files)
{
string temppath = Path.Combine(destDirName, file.Name);
file.CopyTo(temppath, false);
}
// If copying subdirectories, copy them and their contents to new location.
if (copySubDirs)
{
foreach (DirectoryInfo subdir in dirs)
{
string temppath = Path.Combine(destDirName, subdir.Name);
DirectoryCopy(subdir.FullName, temppath, copySubDirs);
}
}
}
}
참고로, 이 문제가 .NET Core 프로젝트에서 NuGet 참조를 한 경우에는 발생하지 않습니다. 왜냐하면, .NET Core 프로젝트는 빌드 시 Managed DLL인 PLplotNet.dll을 빌드 출력 폴더(/bin/debug)에 복사하지 않고 nuget cache 폴더로부터 직접 로드하기 때문입니다.
그건 그렇고, 예제에 보면 PLPLOT_LIB 환경 변수가 나오는데,
_putenv_s("PLPLOT_LIB", supPath);
이게 통하지 않습니다. (혹시 이에 대해 아시는 분은 덧글 부탁드립니다.)
또는 다음과 같은 오류가 발생한다면?
System.BadImageFormatException
HResult=0x8007000B
Message=An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)
Source=PLplotNet
StackTrace:
at PLplot.Native.mkstrm(Int32& p_strm)
at PLplot.PLStream..ctor() in C:\projects\plplotnet\PLplotNet\PLStream.cs:line 23
at ConsoleApp1.Program.Main(String[] args) in E:\ConsoleApp1\ConsoleApp1\Program.cs:line 22
현재 nuget에 올라온 plplot은 네이티브 바이너리가 win-x64와 linux-x64 밖에 없으므로 x86으로 구동하는 응용 프로그램으로는 로드할 수 없습니다. 따라서 자신의 프로젝트에 "Prefer 32-bit" 옵션이 켜져 있거나 "x86" 대상으로 빌드했다면 저렇게
BadImageFormatException이 발생합니다.
만약 x86이 굳이 필요하다면, plplot을 x86으로 빌드해 사용하면 됩니다.
vcpkg가 이미 제공하므로 어렵지 않게 구성할 수 있을 것입니다.
E:\git_clone\vcpkg> vcpkg search plplot
plplot 5.13.0-1 PLplot is a cross-platform software package for creating scientific plots whos...
plplot[wxwidgets] plplot wxwidgets module
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]