2019년 2월 24일 일요일

[서울대 컴퓨터의 개념 및 실습 PA 4] PostScript로 다항함수 그래프 그리기


Programming Assignment #4


*본 보고서는 이미 제출된 적 있는 보고서로 표절 시 발각될 확률이 높으니 참고만 하시길 바랍니다.


<제목 차례>

I. Introduction
1. Program
2. Condtion
2. Programming
1. Background
2. Input & Output
3. Overall Structure
4. Procedure
3. Result
4. Discussion
<그림 차례>
그림 1 점을 회전시키는 그림
그림 2 프로그램 전체, 또는 Main 함수에 대한 실질적인 Input과 Output
그림 3 calculateFunction 함수에 대한 실질적인 Input과 Output
그림 4 rotateFunction에 대한 실질적인 Input과 Output
그림 5 drawFunction 함수에 관한 실질적인 Input과 Output
그림 6 함수 내의 일을 무시하고 표시한 전체적인 구조
그림 7 변수의 선언에 대한 코드
그림 8 Function을 선언하고 파일을 읽어오는 부분 1
그림 9 파일을 읽어오는 부분 2
그림 10 calculateFunction에 대한 부분
그림 11 rotateFunction에 대한 부분
그림 12 drawFunction 중 x와 y의 최소 및 최댓값을 찾는 부분
그림 13 drawFunction 중 PostScript 파일을 작성하는 부분
그림 14 Class Program의 Main 함수
그림 15 회전하지 않은 함수에 대한 결과물 output_x.ps
그림 16 회전한 함수에 대한 결과물 output_o.ps
그림 17 wolframalpha로 그린 plot
<수식 차례>
수식 1 box의 크기에 맞추는 식
수식 2 점을 회전시키는 공식
I. Introduction
1. Problem
임의의 다항함수 가 있다. 주어진 범위 내의 를 postscript를 이용하여 그려라.
2. Condition
이는 프로그래밍을 할 때 만족시켜야 하는 조건이다.
① Programming Language : C#
② input.txt에서 모든 정보를 읽어와야 하며, 그 정보는 다음과 같다.
i. 함수의 계수
ii. 의 범위
iii. 나눌 구간의 개수
iv. Min-Max box 정보
v. 함수의 회전 각도(라디안이 아니라 도로 설정됨)
II. Programming
1. Background
일정 범위의 에서 정의되는 연속함수 에 대해, 의 집합이 정의될 수 있는 최소 크기의 직사각형 범위 이 있다고 했을 때, 의 크기가 어떤 주어진 값이 되도록 의 비율을 조절한다고 하자. 에 속하는 점 , 를 만족하며, 이것이 이제는 , 에 속하도록 범위를 조절해야 한다. 위의 조건을 만족하도록 비율을 조절한 범위 내 집합에 속한 점들의 좌표가 일 때, 아래와 같이 표현할 수 있다.
수식 1 box의 크기에 맞추는 식
또한, 그림 1과 같이 점 으로 구성된 선을 선에 대해 만큼 회전시켰을 때 그 선이 점 라면, 다음과 같은 선형사상을 이용하는 행렬식이 성립한다.
그림 1 점을 회전시키는 그림
수식 2 점을 회전시키는 공식
2. Input & Output
다음은 프로그램 전체와 함수 개별에 관한 실질적인 Input과 Output이다.

그림 2 프로그램 전체, 또는 Main 함수에 대한 실질적인 Input과 Output
input.txt에는 함수에 대한 정보가 들어있으며, 이 정보를 이용하여 Program은 두 개의 출력을 내놓는다. output_o.ps는 회전시킨 함수에 대한 결과물이며, output_x.ps는 회전시키지 않은 함수에 대한 결과물이다. 이 둘을 비교하기 위해서 일부러 2개의 출력물을 생성하도록 하였다.
그림 3 calculateFunction 함수에 대한 실질적인 Input과 Output
x는 함수의 x값을 저장하는 double 배열이고, y는 함수 값을 저장하는 double 배열이다. range는 함수에 대한 x의 범위이며, fragment는 나누는 구간의 개수이고, coefficients는 함수 식에서의 계수를 저장한 double 배열이다. Input을 이용하여 함수를 계산한 뒤, x와 y 배열에 계산 값을 저장한다.
그림 4 rotateFunction에 대한 실질적인 Input과 Output
함수의 x와 y값을 x와 y double 배열을 통해 받아오고, 이미 라디안으로 설정된 angle, 즉 회전각을 가져와서 회전된 함수에 대한 x와 y의 값을 다시 x와 y 배열에 저장한다.
그림 5 drawFunction 함수에 관한 실질적인 Input과 Output
함수의 x와 y 값을 x와 y 배열을 통해 받아오고, 이 함수 선을 box의 크기에 맞게 위의 변수들을 이용하여 크기 조절을 한 뒤, PostScript 파일에 크기 조절을 한 함수 선을 그린다. x와 y 배열에 저장된 값은 바뀌지 않고 남아있게 된다.

3. Overall Structure
후에 Class를 요긴하게 쓰기 위해 함수에 대한 Class를 따로 지정하였다. Class의 이름은 Function이며, 이 Class를 Main 함수에서 불러와서 쓰게 된다.
그림 6 함수 내의 일을 무시하고 표시한 전체적인 구조

4. Procedure
Class Function에 대해 먼저 살펴본다. 우선 변수에 대해서 정의한다.

class Function
    {
        public double[] coefficients;
        public double[] range;
        public int fragment;
        public double box_xmin;
        public double box_xmax;
        public double box_ymin;
        public double box_ymax;
        public double angle;

        public double[] x;

        public double[] y;

그림 7 변수의 선언에 대한 코드
coefficients는 함수의 계수를 저장하는 배열이다. 배열의 주소 값은 해당하는 항의 차수이며, 그곳에 저장된 수는 항의 계수가 된다. range는 함수에 대한 의 범위를 나타내는 배열이다. range[0]은 범위의 최솟값, range[1]은 범위의 최댓값이 된다. fragment는 나눌 구간의 개수를 저장하고, box_xmin은 박스를 그리는 점들의 값 중 가장 작은 값, box_xmax는 가장 큰 값을 저장하며, box_ymin은 박스를 그리는 점들의 값 중 가장 작은 값, box_ymax는 가장 큰 값을 저장한다. (함수를 그리고 박스에 대한 직사각형 또한 그릴 것이다.) angle은 함수를 회전시킬 회전각을 저장하며, 라디안에 해당하는 값을 지닌다. x는 함수의 값을 저장하는 배열이며, y는 함수 값을 저장하는 배열이다.

public Function() { }
public Function(string FileName)
{
            StreamReader sr = new StreamReader(FileName);
            sr.ReadLine();
            string temp;
            string[] tempArr;

            temp = sr.ReadLine();
            tempArr = temp.Split(' ');
            coefficients = new double[tempArr.Length];
            for(int i = 0; i < coefficients.Length; i++)
            {
                coefficients[i] = Convert.ToDouble(tempArr[i]);
            }

            sr.ReadLine();

그림 8 Function을 선언하고 파일을 읽어오는 부분 1
StreamReader을 통해 파일을 읽어오고, 별 뜻이 없는 부분을 미리 읽어버린다. 그 다음에 string 타입 temp와 string 배열 tempArr을 선언하고, temp에 다음 줄을 읽은 것을 저장한다. Split(‘ ‘)을 통해 공백을 기준으로 해서 temp를 나누고, 나눈 부분부분을 double 타입으로 바꾸어서 for문을 이용해 coefficients 배열에 저장한다. coefficients의 크기와 tempArr의 크기는 똑같으니 선언을 할 때 위와 같이 선언한다. 그리고 input.txt에 낮은 차수의 항에 해당하는 계수가 먼저 온다고 했으니 tempArr의 앞에 있는 것을 변환한 값을 coefficients의 앞쪽에 저장한다. 그리고 다음 줄은 쓸모 없으니 미리 읽어버린다.

temp = sr.ReadLine();
            tempArr = temp.Split(' ');
            range = new double[2];
            range[0] = Convert.ToDouble(tempArr[0]);
            range[1] = Convert.ToDouble(tempArr[1]);
            sr.ReadLine();

            temp = sr.ReadLine();
            fragment = Convert.ToInt32(temp);
            sr.ReadLine();

            temp = sr.ReadLine();
            tempArr = temp.Split(' ');
            box_xmin = Convert.ToDouble(tempArr[0]);
            box_ymin = Convert.ToDouble(tempArr[1]);
            box_xmax = Convert.ToDouble(tempArr[2]);
            box_ymax = Convert.ToDouble(tempArr[3]);
            sr.ReadLine();

            temp = sr.ReadLine();
            angle = Convert.ToDouble(temp);
            angle = Math.PI * angle / 180;
            sr.Close();

}

그림 9 파일을 읽어오는 부분 2
그 다음 줄은 x range에 관한 내용이므로 읽어서 temp에 저장한다. 마찬가지로 공백을 기준으로 나눈 temp를 tempArr에 저장한다. 앞의 수는 범위의 최솟값이고, 뒤의 수는 범위의 최댓값임을 고려해서 tempArr의 성분을 double 타입으로 변환한 값을 위와 같이 range 배열에 저장한다. 다음 줄은 쓸모 없는 내용이므로 미리 읽어버린다.
그 다음 줄은 나눌 구간의 개수에 관한 내용이므로 temp에 저장하고 temp를 int32 타입으로 변환하여 fragment에 저장한다. 다음 줄은 쓸모 없는 내용이므로 미리 읽어버린다.
그 다음 줄은 박스의 위치와 크기에 관한 내용이다. 같은 방법으로 내용을 저장하고, tempArr의 첫 번째 성분은 box_xmin에, 두 번째는 box_ymin에, 세 번째는 box_xmax에, 네 번째는 box_ymax에 저장한다. 다음 줄은 쓸모 없는 내용이므로 미리 읽어버린다.
그 다음 줄은 회전각에 관한 내용이다 읽은 내용을 temp에 저장하고, temp를 double 타입으로 변환한 값을 angle에 저장한다. 이때 angle은 라디안 값이 아니라 도 형식으로 돼 있으므로, Math.PI의 존재를 이용해 라디안으로 변환한다. 마지막으로 StreamReader을 닫는다.

public void calculateFunction()
{
x = new double[fragment + 1];
            y = new double[fragment + 1];
            double dx = (range[1] - range[0]) / (double)fragment;

            x[0] = range[0];

            y[0] = 0;
for(int i = 0; i < coefficients.Length; i++)
{
y[0] += coefficients[i] * Math.Pow(x[0], i);
}

for(int i = 1; i < (fragment + 1); i++)
{
x[i] = x[i - 1] + dx;
                      y[i] = 0;
                      for (int j = 0; j < coefficients.Length; j++)
                     {
                          y[i] += coefficients[j] * Math.Pow(x[i], j);
                     }
}
}

그림 10 calculateFunction에 대한 부분
배열 x와 y를 초기화한다. 그리고 구간에 대한 x의 간극 dx를 계산한다. 먼저 x[0]을 range[0], y[0]을 0으로 설정하고, for문을 이용하여 y[0]을 계산한다. for문이 한 번씩 돌아갈 때마다 i를 차수로 가지는 항이 차례차례 더해진다. 그리고 이중 for문을 이용해서 나머지 함수 값을 구한다. 바깥의 for문이 돌아갈 때마다 x[i]에는 x[i-1]에 dx를 더한 값이 저장되고, y[i]에는 내부의 for문으로 계산된 함수 값이 저장된다.

public void rotateFunction()
{
     double tempx, tempy;
     for(int i = 0; i < x.Length; i++)
     {
           tempx = x[i];
           tempy = y[i];
           x[i] = (tempx * Math.Cos(angle)) - (tempy * Math.Sin(angle));
           y[i] = (tempx * Math.Sin(angle)) + (tempy * Math.Cos(angle));
      }

}

그림 11 rotateFunction에 대한 부분
원래의 x와 y값을 각각 저장할 double 타입의 변수 tempx와 tempy를 선언한다. 그 후에 for문을 돌리는데, for문은 x[i]의 값을 잠깐 tempx에, y[i]의 값을 잠깐 tempy에 저장하고, 그 값을 이용해서 수식 2의 값을 본래 x[i]와 y[i]가 있던 자리에 저장한다. 그러면 x 배열과 y 배열에는 회전한 점들의 좌표가 저장된다.

public void drawFunction(string fileName)
{
          double xmin, xmax, ymin, ymax;

          xmin = x[0];
          xmax = x[0];
for(int i = 1; i < x.Length; i++)
            {
                if (x[i] < xmin)
                {
                    xmin = x[i];
                }
                else if(x[i] > xmax)
                {
                    xmax = x[i];
                }
            }

            ymin = y[0];
            ymax = y[0];
            for (int i = 1; i < y.Length; i++)
            {
                if (y[i] < ymin)
                {
                    ymin = y[i];
                }
                else if (y[i] > ymax)
                {
                    ymax = y[i];
                }
            }

그림 12 drawFunction 중 x와 y의 최소 및 최댓값을 찾는 부분
double 타입의 변수 xmin, xmax, ymin, ymax를 선언하는데, 각각 x 배열 원소 중 가장 최소와 최댓값, 그리고 y 배열 원소 중 가장 최소와 최댓값이 저장될 변수들이다. 우선, xmin과 xmax에 x[0] 값을 미리 집어넣는다. 그리고 for문을 돌리면서 x[i]가 xmin 보다 작으면 x[i]를 대신 xmin에 대입하고, x[i]가 xmax보다 크면 x[i]를 xmax에 대신 대입하는 것을 반복한다. y에 대해서도 같은 작업을 반복하면 최소와 최댓값을 모두 찾을 수 있다.

StreamWriter sw = new StreamWriter(fileName);
            sw.WriteLine("newpath");
            string print = String.Format("{0} {1} moveto", box_xmin + ((box_xmax - box_xmin) * (x[0] - xmin) / (xmax - xmin)), box_ymin + ((box_ymax - box_ymin) * (y[0] - ymin) / (ymax - ymin)));
            sw.WriteLine(print);
            for (int i = 1; i < x.Length; i++)
            {
                print = String.Format("{0} {1} lineto", box_xmin + ((box_xmax - box_xmin) * (x[i] - xmin) / (xmax - xmin)), box_ymin + ((box_ymax - box_ymin) * (y[i] - ymin) / (ymax - ymin)));
                sw.WriteLine(print);
            }
            sw.WriteLine("stroke");
            sw.WriteLine("{0} {1} moveto", box_xmin, box_ymin);
            sw.WriteLine("{0} {1} lineto", box_xmax, box_ymin);
            sw.WriteLine("{0} {1} lineto", box_xmax, box_ymax);
            sw.WriteLine("{0} {1} lineto", box_xmin, box_ymax);
            sw.WriteLine("{0} {1} lineto", box_xmin, box_ymin);
            sw.WriteLine("stroke");
            sw.Close();

        }

그림 13 drawFunction 중 PostScript 파일을 작성하는 부분
StreamWriter을 선언하고, 수식 1을 이용하여 계산한 좌표들을 작성한다. 이때, 는 연속이므로, for문을 이용해서 lineto로 (x[i], y[i])를 차례차례 이어가도 하나의 선을 그리는데 성공할 것이다. 함수에 대한 선을 그린 뒤에는 이를 둘러싸는 직사각형을 그려서 함수의 plot을 그리는 것에 성공했는지 확인할 수 있도록 한다. 마지막으로 StreamWriter을 닫는다.

class Program
{
   static void Main(string[] args)
        {
            Function function = new Function("input.txt");
            function.calculateFunction();
            function.drawFunction("output_x.ps");
            function.rotateFunction();
            function.drawFunction("output_o.ps");
        }

}

그림 14 Class Program의 Main 함수
input.txt를 입력하고, 함수를 계산한 뒤에 회전시키지 않은 상태에서 output_x.ps를 작성하고, 함수를 회전시킨 뒤에 output_o.ps를 작성해서 회전시키지 않은 그림과 회전시킨 그림을 비교할 수 있도록 한다.
III. Result
그림 15 회전하지 않은 함수에 대한 결과물 output_x.ps
그림 16 회전한 함수에 대한 결과물 output_o.ps
그리고 회전하지 않은 함수에 대한 plot을 wolframalpha로 확인한 결과는 다음과 같다.
그림 17 wolframalpha로 그린 plot
PostScript로 그린 결과와 유사한 것을 알 수 있다. 비교해 보았을 때, PostScript로 함수의 개형을 그리는데 성공했다고 판단할 수 있었다.
IV. Discussion
drawFunction 함수 중 PostScript를 작성하는 부분에서 WriteLine의 괄호 안에 위의 코드 중 String.Format 괄호 안에 있던 내용물을 그대로 집어넣으면 String.FormatException이 있다면서 프로그램이 오류를 일으키는 모습을 볼 수 있었다. 앞으로는 String.Format을 자주 이용해서 그런 일이 없도록 해야 하겠다는 생각이 들었다.