Python으로 배우는 DSP/Part 3. 디지털 필터 설계와 구현

[15] Z-변환(Z-Transform) 기초: 이산 시스템의 표현과 ROC(수렴 영역) 이해

multimedia 2026. 5. 16. 10:00
반응형

들어가며

이전 시간에는 시간 영역에서 필터가 신호에 작용하는 원리를 컨볼루션(Convolution)이라는 연산을 통해 살펴보았습니다. 출력이 과거 입력값들의 가중 합산이라는 사실, 그리고 임펄스 응답(Impulse Response)이 시스템의 특성을 완전히 기술한다는 사실을 확인했습니다.

하지만 한 가지 근본적인 문제에 직면하게 됩니다. 시간 축에서 두 신호를 일일이 곱하고 더하는 컨볼루션 연산은 직관적이긴 하지만, 시스템이 복잡해질수록 계산량이 기하급수적으로 늘어납니다. 또한, 시간 영역의 신호 모양만으로는 이 필터가 특정 주파수를 어떻게 통과시키는지, 혹은 이 시스템이 안정적인지 불안정한지 한눈에 파악하기가 매우 어렵습니다.

이 복잡한 시간 영역의 컨볼루션을 단순한 주파수 영역의 '곱셈'으로 변환하고, 필터 분석을 체계화하는 강력한 수학적 도구가 필요합니다. 연속 시간 신호에 라플라스 변환(Laplace Transform)이 있다면, 이산 시간 신호(Discrete-time Signal)에는 바로 Z-변환(Z-Transform)이 있습니다.

이번 편에서는 Z-변환의 정의와 수렴 영역(ROC), 역 Z-변환, 주요 성질, 그리고 단위 원(Unit Circle)과 DTFT의 관계를 Python 시각화와 함께 차근차근 살펴보겠습니다.

 

1. Z-변환의 정의와 물리적 의미

Z-변환은 시간 영역의 이산 신호 $x[n]$을 복소수 평면인 Z-도메인(Z-domain)으로 매핑하는 수학적 연산입니다. 수학적 정의는 다음과 같습니다.

$$X(z) = \sum_{n=-\infty}^{\infty} x[n] z^{-n}$$

여기서 $z$는 복소수(Complex Number) 변수입니다. 복소수는 크기(magnitude)와 위상(phase)을 가지므로, 극좌표계를 사용하여 다음과 같이 표현할 수 있습니다.

$$z = r e^{j\omega}$$

위 수식에서 $r$은 크기(magnitude), $\omega$는 각주파수(angular frequency)입니다. 이를 Z-변환 공식에 대입하면 다음과 같은 형태가 됩니다.

$$X(r e^{j\omega}) = \sum_{n=-\infty}^{\infty} (x[n] r^{-n}) e^{-j\omega n}$$

이 수식은 매우 중요한 물리적 의미를 담고 있습니다.

단순한 이산 신호 $x[n]$을 분석하는 것이 아니라, 원래 신호에 $r^{-n}$이라는 지수 함수(Exponential function)를 곱한 새로운 신호를 푸리에 변환하는 것과 같습니다. 지수 함수를 곱함으로써, 진동하거나 발산하여 원래는 푸리에 변환을 할 수 없는 불안정한 신호들까지도 분석할 수 있게 만들어 주는 것이 바로 Z-변환의 진정한 힘입니다.

실제 인과(causal) 시스템을 다룰 때는 $n < 0$인 구간에서 $x[n] = 0$인 단방향(unilateral) Z-변환을 주로 사용합니다.

$$X(z) = \sum_{n=0}^{\infty} x[n] z^{-n}$$

간단한 예시: 단위 임펄스와 단위 계단 신호

가장 기본적인 두 신호의 Z-변환을 직접 계산해 보겠습니다.

단위 임펄스 신호 $\delta[n]$:

$$\mathcal{Z}\{\delta[n]\} = \sum_{n=0}^{\infty} \delta[n] z^{-n} = 1$$

$n = 0$일 때만 $\delta[0] = 1$이므로, Z-변환은 단순히 1이 됩니다. 모든 $z$에서 수렴합니다.

단위 계단 신호 $u[n]$:

$$\mathcal{Z}\{u[n]\} = \sum_{n=0}^{\infty} z^{-n} = \frac{1}{1 - z^{-1}} = \frac{z}{z - 1}, \quad |z| > 1$$

이 급수는 등비급수 공식으로 계산되며, $|z^{-1}| < 1$, 즉 $|z| > 1$인 조건에서만 수렴합니다.

 

2. 수렴 영역 (ROC, Region of Convergence)

Z-변환 공식은 $n = -\infty$부터 $\infty$까지의 무한급수입니다. 무한히 더하는 연산이기 때문에, 이 합이 무한대로 발산하지 않고 특정한 유한한 값을 가지기 위한 조건이 반드시 필요합니다.

급수가 발산하지 않고 수렴하도록 하여 유한한 값을 만드는 복소수 $z$의 크기($|z| = r$) 범위를 수렴 영역(Region of Convergence, ROC)이라고 부릅니다.

ROC는 다음과 같은 중요한 특성을 가집니다.

  • ROC는 원환(annular region) 형태입니다: $r_1 < |z| < r_2$
  • 유한 길이(FIR) 신호의 ROC는 $z = 0$ 또는 $z = \infty$를 제외한 전체 $z$ 평면인 경우가 많습니다.
  • 인과적이고 안정적인 시스템의 경우, ROC는 단위 원(Unit Circle, $|z| = 1$)을 포함해야 합니다.
  • ROC를 명시하지 않으면 Z-변환만으로는 원래 신호를 유일하게 복원할 수 없습니다.

ROC는 신호의 특성에 따라 다음과 같이 형태가 달라집니다.

  • 인과 신호 (Causal Sequence): $n \ge 0$에서만 값이 있는 신호. ROC는 어떤 원의 바깥쪽이 됩니다. ($|z| > a$)
    $$x_1[n] = a^n u[n] \quad \Leftrightarrow \quad X(z) = \frac{z}{z - a}, \quad |z| > |a| $$
  • 반인과 신호 (Anti-causal Sequence): $n < 0$에서만 값이 있는 신호. ROC는 어떤 원의 안쪽이 됩니다. ($|z| < a$)
    $$x_2[n] = -a^n u[-n - 1] \quad \Leftrightarrow \quad X(z) = \frac{z}{z - a}, \quad |z| < |a| $$
  • 양방향 신호 (Two-sided Sequence): 양쪽 모두에 값이 있는 신호. ROC는 두 원 사이의 고리(Ring) 형태가 됩니다. ($a < |z| < b$)

이처럼 ROC는 X(z)의 수식만큼이나 중요한 정보입니다. 아래 Python 코드로 복소 평면 위에 ROC를 시각화해 보겠습니다.

import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False

fig, axes = plt.subplots(1, 2, figsize=(12, 5))
fig.suptitle('Z-변환의 수렴 영역(ROC) 비교', fontsize=14, fontweight='bold')

a = 0.5
theta = np.linspace(0, 2 * np.pi, 500)

# --- 좌측: 인과 신호 ROC: |z| > 0.5 (바깥 무한 영역) ---
ax = axes[0]
# 전체 배경을 ROC 색으로 채움
ax.set_facecolor('skyblue')
ax.patch.set_alpha(0.3)
# 극점 안쪽 원(|z| ≤ 0.5)을 흰색으로 덮어서 제외 영역 표현
ax.fill(a * np.cos(theta), a * np.sin(theta), color='white')
# 단위 원
ax.plot(np.cos(theta), np.sin(theta), 'k--', linewidth=1.5, label='단위 원 (|z|=1)')
# 극점
ax.plot(a, 0, 'rx', markersize=12, markeredgewidth=2, label=f'극점 (z={a})')
# 경계 원
ax.plot(a * np.cos(theta), a * np.sin(theta), 'b-', linewidth=1.2, label=f'|z|={a} 경계')

ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
ax.set_aspect('equal')
ax.axhline(0, color='gray', linewidth=0.5)
ax.axvline(0, color='gray', linewidth=0.5)
ax.set_xlabel('실수축 (Re)')
ax.set_ylabel('허수축 (Im)')
ax.set_title('인과 신호: $x_1[n] = (0.5)^n u[n]$\nROC: |z| > 0.5 (외부 무한 영역)', fontsize=11)
ax.legend(loc='upper right', fontsize=9)
ax.grid(True, linestyle=':', alpha=0.5)

# --- 우측: 반인과 신호 ROC: |z| < 0.5 (안쪽 유한 영역) ---
ax = axes[1]
# 안쪽 원만 ROC 색으로 채움
ax.fill(a * np.cos(theta), a * np.sin(theta), color='salmon', alpha=0.3, label='ROC 영역')
# 단위 원
ax.plot(np.cos(theta), np.sin(theta), 'k--', linewidth=1.5, label='단위 원 (|z|=1)')
# 극점
ax.plot(a, 0, 'rx', markersize=12, markeredgewidth=2, label=f'극점 (z={a})')
# 경계 원
ax.plot(a * np.cos(theta), a * np.sin(theta), 'b-', linewidth=1.2, label=f'|z|={a} 경계')

ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
ax.set_aspect('equal')
ax.axhline(0, color='gray', linewidth=0.5)
ax.axvline(0, color='gray', linewidth=0.5)
ax.set_xlabel('실수축 (Re)')
ax.set_ylabel('허수축 (Im)')
ax.set_title('반인과 신호: $x_2[n] = -(0.5)^n u[-n-1]$\nROC: |z| < 0.5 (내부 유한 영역)', fontsize=11)
ax.legend(loc='upper right', fontsize=9)
ax.grid(True, linestyle=':', alpha=0.5)

plt.tight_layout()
plt.savefig('roc_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

좌측 그래프는 인과 신호의 ROC로, 극점 $z = 0.5$ 바깥쪽($|z| > 0.5$) 영역이 ROC이며 단위 원을 포함합니다. 우측은 반인과 신호의 ROC로, 극점 안쪽($|z| < 0.5$) 영역이 ROC이며 단위 원을 포함하지 않습니다.

 

3. Z-변환의 기본 성질

Z-변환은 선형 변환이므로 디지털 필터 설계와 시스템 분석을 쉽게 만들어주는 강력한 수학적 성질들을 가지고 있습니다. 

3.1. 선형성 (Linearity)

시간 영역에서 두 신호를 더하거나 상수를 곱한 것은 Z-도메인에서도 동일하게 유지됩니다. ROC는 두 신호의 ROC의 교집합 이상이 됩니다.

$$\mathcal{Z}\{a \cdot x_1[n] + b \cdot x_2[n]\} = a \cdot X_1(z) + b \cdot X_2(z)$$

3.2. 시간 지연 (Time Delay)

DSP에서 가장 빈번하게 사용되는 성질입니다. 시간 축에서 신호를 1샘플 지연시키는 것은 Z-도메인에서 $z^{-1}$을 곱하는 것과 완벽히 동일합니다.

$$\mathcal{Z}\{x[n - k]\} = z^{-k} X(z)$$

디지털 필터를 소프트웨어로 구현할 때 과거의 데이터($x[n-1]$ 등)를 저장해두는 버퍼 메모리들이 수식에서는 모조리 $z^{-1}$로 치환되어, 복잡한 미적분이나 차분 방정식이 단순한 대수학 연산으로 바뀝니다.

3.3. 컨볼루션과 곱셈의 대응

시간 영역의 컨볼루션은 Z-변환 영역에서 단순 곱셈으로 변환됩니다. 이 성질 덕분에 필터의 동작을 전달함수 $H(z)$로 간결하게 표현할 수 있습니다.

$$\mathcal{Z}\{x[n] * h[n]\} = X(z) \cdot H(z)$$

3.4. z-영역 스케일링

신호에 지수 시퀀스 $a^n$을 곱하면, Z-변환에서는 $z$ $z/a$로 대체하는 것에 해당합니다.

$$\mathcal{Z}\{a^n x[n]\} = X \left( \frac{z}{a} \right)$$

 

4. 역 Z-변환 (Inverse Z-Transform)

Z-도메인에서 분석이나 필터 설계를 마쳤다면, 이를 다시 시간 영역의 신호나 소프트웨어 코드로 돌려놓아야 합니다. Z-변환의 역연산, 즉 $X(z)$로부터 원래 신호 $x[n]$을 복원하는 방법을 역 Z-변환이라고 합니다. 수학적인 정의는 복소 평면상의 폐곡선 적분(Contour Integration)을 사용하지만, 실무에서는 거의 계산하지 않습니다.

$$x[n] = \frac{1}{2\pi j} \oint_C X(z)z^{n-1} dz$$

대신 부분 분수 전개법(Partial Fraction Expansion)을 주로 사용합니다. 복잡한 형태의 $X(z)$를 이미 결과를 알고 있는 단순한 기본 분수들의 합으로 쪼갠 뒤, Z-변환 표(Table)를 보고 역으로 시간 영역 신호 $x[n]$을 조립하는 방식입니다.

예를 들어 다음과 같은 $X(z)$를 생각해 보겠습니다.

$$X(z) = \frac{1}{(1 - 0.5z^{-1})(1 - 0.25z^{-1})}, \quad |z| > 0.5$$

이를 부분 분수로 분해하면:

$$X(z) = \frac{A}{1 - 0.5z^{-1}} + \frac{B}{1 - 0.25z^{-1}}$$

계수를 계산하면 $A = 2, B = -1$이 되고, 따라서:

$$x[n] = 2 \cdot (0.5)^n u[n] - (0.25)^n u[n]$$

Python 환경에서는 scipy.signal.residuez 함수를 사용하여 이러한 부분 분수 전개를 단 한 줄의 코드로 쉽게 처리할 수 있습니다.

import numpy as np
from scipy import signal

# X(z) = 1 / [(1 - 0.5z^{-1})(1 - 0.25z^{-1})]
# 분자: 1, 분모: (1 - 0.5z^{-1})(1 - 0.25z^{-1}) 전개
# 분모 = 1 - 0.75z^{-1} + 0.125z^{-2}
b = [1]                   # 분자 계수
a = [1, -0.75, 0.125]     # 분모 계수

r, p, k = signal.residuez(b, a)

print("=== 부분 분수 분해 결과 ===")
for i, (res, pole) in enumerate(zip(r, p)):
    print(f"  항 {i+1}: 잔류(residue) = {res:.4f}, 극점(pole) = {pole:.4f}")
print(f"  직접항(k) = {k}")

# 역 Z-변환 결과 검증: x[n] = 2*(0.5)^n*u[n] - 1*(0.25)^n*u[n]
n = np.arange(20)
x_reconstructed = r[0] * (p[0] ** n) + r[1] * (p[1] ** n)

# 임펄스 응답과 비교
impulse_input = np.zeros(20)
impulse_input[0] = 1
x_impulse = signal.lfilter(b, a, impulse_input)

print("\n=== 역 Z-변환 검증 (처음 10샘플) ===")
print(f"  부분 분수 복원: {x_reconstructed[:10].round(4)}")
print(f"  임펄스 응답:    {x_impulse[:10].round(4)}")
=== 부분 분수 분해 결과 ===
  항 1: 잔류(residue) = -1.0000, 극점(pole) = 0.2500
  항 2: 잔류(residue) = 2.0000, 극점(pole) = 0.5000
  직접항(k) = []

=== 역 Z-변환 검증 (처음 10샘플) ===
  부분 분수 복원: [1.     0.75   0.4375 0.2344 0.1211 0.0615 0.031  0.0156 0.0078 0.0039]
  임펄스 응답:    [1.     0.75   0.4375 0.2344 0.1211 0.0615 0.031  0.0156 0.0078 0.0039]

실행 결과로 얻은 부분 분수 분해 결과로부터 역변환된 임펄스 응답은 다음과 같습니다.

$$X(z) = \frac{\color{red}{R_1}}{1 - 0.5z^{-1}} + \frac{\color{red}{R_2}}{1 - 0.25z^{-1}}$$

여기서 $\color{red}{R_1 = 2}, \color{red}{R_2 = -1}$이 바로 잔류(residue)입니다.

각 항의 역 Z-변환 공식은 다음과 같습니다.

$$\frac{R_k}{1 - p_k z^{-1}} \xrightarrow{\mathcal{Z}^{-1}} R_k \cdot p_k^n u[n]$$

따라서 최종 역 Z-변환 결과는:

$$x[n] = \underbrace{2}_{R_1} \cdot \underbrace{(0.5)^n}_{p_1^n} u[n] \underbrace{- 1}_{R_2} \cdot \underbrace{(0.25)^n}_{p_2^n} u[n]$$

즉 잔류는 각 지수 성분의 가중치(크기와 부호)를 결정합니다.

또한, 위 코드의 실행 결과로부터 부분 분수로 복원한 값과 임펄스 응답이 완전히 일치하는 것을 확인할 수 있습니다.

 

5. 주요 Z-변환 쌍 정리

자주 사용하는 Z-변환 쌍을 표로 정리합니다. 역 Z-변환 시 참고 기준으로 활용하시기 바랍니다.

시간 영역 $x[n]$ Z-변환 $X(z)$ ROC
$\delta[n]$ $1$ 전체 $z$ 평면
$u[n]$ $\displaystyle \frac{z}{z - 1}$ $|z| > 1$
$a^n u[n]$ $\displaystyle \frac{z}{z - a}$ $|z| > |a|$
$n \cdot a^n u[n]$ $\displaystyle \frac{az^{-1}}{(1 - az^{-1})^2}$ $|z| > |a|$
$\cos(\omega_0 n) u[n]$ $\displaystyle \frac{1 - \cos(\omega_0)z^{-1}}{1 - 2\cos(\omega_0)z^{-1} + z^{-2}}$ $|z| > 1$
$r^n \cos(\omega_0 n) u[n]$ $\displaystyle \frac{1 - r \cos(\omega_0)z^{-1}}{1 - 2r \cos(\omega_0)z^{-1} + r^2 z^{-2}}$ $|z| > r$

 

6. Z-변환과 단위원, 그리고 DTFT 

앞서 복소수 $z = r e^{j\omega}$라고 정의했습니다. 만약 크기 $r$이 1이라면 어떻게 될까요?

복소 평면에서 원점에서 거리가 1인 점들의 모임을 **단위원(Unit Circle, $|z|=1$)**이라고 합니다.

Z-변환 공식에 $z = e^{j\omega}$를 대입해 보겠습니다.

$$X(e^{j\omega}) = \sum_{n=-\infty}^{\infty} x[n] e^{-j\omega n}$$

이 수식은 신호 처리에서 주파수 분석을 위해 사용하는 이산 시간 푸리에 변환(DTFT, Discrete-Time Fourier Transform) 공식과 완전히 동일합니다.

즉, DTFT는 Z-변환을 단위원($|z|=1$) 위에서만 평가한 특수한 경우입니다. 시스템의 주파수 응답을 알고 싶다면 Z-도메인의 단위원 위를 한 바퀴 걸어 다니며 그 값을 읽어내기만 하면 됩니다. 단, 이 주파수 응답이 유효하려면 시스템의 ROC 안에 단위원이 반드시 포함되어 있어야만 합니다. 이 관계는 다음과 같은 중요한 의미를 가집니다.

  • 단위 원(Unit Circle)이 ROC 안에 있으면 DTFT가 존재하며, 시스템이 안정적(Stable)임을 의미합니다.
  • 단위 원 위의 $z$ 값은 각각의 정규화 주파수 $\omega \in [-\pi, \pi]$에 대응됩니다.
  • 복소 평면에서 극점(pole)이 단위 원에 가까울수록 해당 주파수 근방에서 주파수 응답의 크기가 커집니다.
  • 반대로 영점(zero)이 단위 원 위에 있으면 해당 주파수 성분이 완전히 제거됩니다.

아래 Python 코드로 단위 원과 주파수 응답의 관계를 함께 시각화해 보겠습니다.

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False

# 간단한 IIR 필터 정의: 극점 r=0.9, 각도 π/4 (π/4 대역 강조 필터)
r = 0.9
omega0 = np.pi / 4
# 켤레 극점 쌍: z = r*e^{±j*omega0}
# 분모: (z - r*e^{jω0})(z - r*e^{-jω0}) = z^2 - 2r*cos(ω0)*z + r^2
a = [1, -2 * r * np.cos(omega0), r**2]
b = [1]

# 주파수 응답
w, h = signal.freqz(b, a, worN=512)

# 극점/영점
zeros, poles, _ = signal.tf2zpk(b, a)

fig, axes = plt.subplots(1, 2, figsize=(13, 5))
fig.suptitle('단위 원과 DTFT(주파수 응답)의 관계', fontsize=14, fontweight='bold')

# --- 좌측: 극점·영점 및 단위 원 ---
ax1 = axes[0]
theta = np.linspace(0, 2 * np.pi, 500)
ax1.plot(np.cos(theta), np.sin(theta), 'k--', linewidth=1.5, label='단위 원')

# 극점
ax1.plot(poles.real, poles.imag, 'rx', markersize=12,
         markeredgewidth=2, label=r'극점 ($r=0.9$, $\omega_0=\pi/4$)')

# 영점 (z=0)
ax1.plot(zeros.real, zeros.imag, 'bo', markersize=10,
         markerfacecolor='none', markeredgewidth=2, label='영점')

# 단위 원 위의 샘플 주파수 표시
sample_omegas = [0, np.pi/4, np.pi/2, 3*np.pi/4, np.pi]
for om in sample_omegas:
    ax1.plot(np.cos(om), np.sin(om), 'g^', markersize=8)
    ax1.annotate(f'ω={om/np.pi:.2f}π',
                 (np.cos(om), np.sin(om)),
                 textcoords='offset points', xytext=(8, 5), fontsize=8)

ax1.set_xlim(-1.5, 1.5)
ax1.set_ylim(-1.5, 1.5)
ax1.set_aspect('equal')
ax1.axhline(0, color='gray', linewidth=0.5)
ax1.axvline(0, color='gray', linewidth=0.5)
ax1.set_xlabel('실수축 (Re)')
ax1.set_ylabel('허수축 (Im)')
ax1.set_title('복소 z-평면: 극점·영점 분포')
ax1.legend(fontsize=9, loc='lower right')
ax1.grid(True, linestyle=':', alpha=0.5)

# --- 우측: 주파수 응답 (크기) ---
ax2 = axes[1]
ax2.plot(w / np.pi, 20 * np.log10(np.abs(h)), 'b-', linewidth=2)
ax2.axvline(omega0 / np.pi, color='r', linestyle='--',
            label=r'극점 주파수 $\omega_0=\pi/4$ (0.25$\pi$)')
ax2.set_xlabel('정규화 주파수 (×π rad/sample)')
ax2.set_ylabel('크기 (dB)')
ax2.set_title('주파수 응답: |H(e^{jω})|')
ax2.legend(fontsize=9)
ax2.grid(True, linestyle=':', alpha=0.5)
ax2.set_xlim(0, 1)

plt.tight_layout()
plt.savefig('unit_circle_dtft.png', dpi=150, bbox_inches='tight')
plt.show()

좌측 그래프에서 극점은 단위 원의 안쪽($r = 0.9$), 정규화 주파수 $\omega_0 = \pi/4$ 방향에 위치합니다. 우측 그래프의 주파수 응답을 보면 $\omega = 0.25\pi$ 부근에서 응답 크기가 최대가 되는 것을 확인할 수 있습니다. 극점이 단위 원에 가까울수록 해당 주파수의 이득(gain)이 증가한다는 원리가 그대로 반영된 결과입니다.

 

7. 종합 예제: Z-변환으로 차분 방정식 분석하기

실제 필터 설계에서는 Z-변환을 이용하여 차분 방정식(Difference Equation)을 전달함수 $H(z)$로 변환하고, 이를 기반으로 시스템의 특성을 분석합니다.

다음 차분 방정식을 생각해 보겠습니다.

$$y[n] = x[n] + 0.5 y[n - 1]$$

양변에 Z-변환을 적용하고, 시간 지연 성질 $\mathcal{Z}\{y[n - 1]\} = z^{-1} Y(z)$를 이용합니다.

$$Y(z) = X(z) + 0.5 z^{-1} Y(z)$$
$$Y(z)(1 - 0.5 z^{-1}) = X(z)$$
$$H(z) = \frac{Y(z)}{X(z)} = \frac{1}{1 - 0.5 z^{-1}} = \frac{z}{z - 0.5}, \quad |z| > 0.5$$

이 시스템의 임펄스 응답은 $h[n] = (0.5)^n u[n]$으로, 지수적으로 감쇠하는 안정적인 시스템입니다. Python으로 전달함수로부터 시스템의 동작을 직접 확인해 보겠습니다.

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False

# 전달함수 H(z) = 1 / (1 - 0.5z^{-1})
b = [1]          # 분자: 1
a = [1, -0.5]    # 분모: 1 - 0.5z^{-1}

n_samples = 30
n = np.arange(n_samples)

# 이론적 임펄스 응답
h_theory = (0.5) ** n

# scipy로 계산한 임펄스 응답
impulse_input = np.zeros(n_samples)
impulse_input[0] = 1
h_scipy = signal.lfilter(b, a, impulse_input)

# 주파수 응답
w, H = signal.freqz(b, a, worN=512)

fig, axes = plt.subplots(1, 3, figsize=(15, 4))
fig.suptitle('H(z) = z/(z-0.5) 시스템 분석', fontsize=13, fontweight='bold')

# 임펄스 응답
ax1 = axes[0]
ax1.stem(n, h_scipy, linefmt='b-', markerfmt='bo', basefmt='gray')
ax1.plot(n, h_theory, 'r--', linewidth=1.5, label='이론값 $(0.5)^n$')
ax1.set_title('임펄스 응답 h[n]')
ax1.set_xlabel('샘플 인덱스 n')
ax1.set_ylabel('진폭')
ax1.legend(fontsize=9)
ax1.grid(True, linestyle=':', alpha=0.5)

# 주파수 응답 (크기)
ax2 = axes[1]
ax2.plot(w / np.pi, 20 * np.log10(np.abs(H) + 1e-10), 'b-', linewidth=2)
ax2.set_title('주파수 응답 크기 |H(e^{jω})|')
ax2.set_xlabel('정규화 주파수 (×π rad/sample)')
ax2.set_ylabel('크기 (dB)')
ax2.grid(True, linestyle=':', alpha=0.5)
ax2.set_xlim(0, 1)

# 극점·영점 및 단위 원
ax3 = axes[2]
theta_c = np.linspace(0, 2 * np.pi, 500)
ax3.plot(np.cos(theta_c), np.sin(theta_c), 'k--', linewidth=1.5, label='단위 원')

zeros_pz, poles_pz, _ = signal.tf2zpk(b, a)
ax3.plot(poles_pz.real, poles_pz.imag, 'rx', markersize=12,
         markeredgewidth=2.5, label=f'극점 (z=0.5)')
if len(zeros_pz) > 0:
    ax3.plot(zeros_pz.real, zeros_pz.imag, 'bo', markersize=10,
             markerfacecolor='none', markeredgewidth=2, label='영점')

ax3.set_xlim(-1.5, 1.5)
ax3.set_ylim(-1.5, 1.5)
ax3.set_aspect('equal')
ax3.axhline(0, color='gray', linewidth=0.5)
ax3.axvline(0, color='gray', linewidth=0.5)
ax3.set_title('극점·영점 분포 (z-평면)')
ax3.set_xlabel('실수축 (Re)')
ax3.set_ylabel('허수축 (Im)')
ax3.legend(fontsize=9)
ax3.grid(True, linestyle=':', alpha=0.5)

plt.tight_layout()
plt.savefig('difference_equation_analysis.png', dpi=150, bbox_inches='tight')
plt.show()

# 안정성 판별
print("=== 시스템 안정성 판별 ===")
for i, pole in enumerate(poles_pz):
    mag = np.abs(pole)
    status = "안정 (|z| < 1)" if mag < 1 else "불안정 (|z| ≥ 1)"
    print(f"  극점 {i+1}: z = {pole:.4f}, |z| = {mag:.4f} → {status}")

그래픽 출력에서는 임펄스 응답과 주파수 응답을 시간축·주파수축으로 각각 도시하였고, 안정성 정보를 시각적으로 보여주기 위해 단위원과 극점을 도시하였습니다. 극점이 단위 원 안쪽($|z| < 1$)에 위치하므로 시스템은 안정적입니다. 임펄스 응답도 지수적으로 0에 수렴하는 것을 확인할 수 있습니다.

다음은 코드 내에서 시스템의 안정성을 판별하기 위해 극점의 위치가 단위원 내부에 있는지 외부에 있는지 판별하여 안정성 결과를 출력한 내용입니다.

=== 시스템 안정성 판별 ===
  극점 1: z = 0.5000, |z| = 0.5000 → 안정 (|z| < 1)

 

[Insight]

시간 영역에서 $y[n] = x[n] * h[n]$ 이라는 무겁고 골치 아픈 컨볼루션 연산은 Z-변환을 거치고 나면 단순한 곱셈이 됩니다.

$$Y(z) = X(z) \cdot H(z)$$

우리는 시간의 흐름이라는 1차원적인 시야에서 벗어나, Z-평면이라는 2차원 공간 위로 올라오게 되었습니다. 이 2차원 지도 위에서 시스템(필터)은 특정한 위치에 봉우리(극점, Pole)를 세우고 골짜기(영점, Zero)를 파놓은 지형도와 같습니다.

단순히 수식을 계산하기 위해 Z-변환을 도입한 것이 아닙니다. 이 지형도를 내려다봄으로써 우리는 복잡한 계산 없이도 봉우리의 위치만 보고 "이 시스템은 발산하여 터져버리겠군", 혹은 "이 필터는 저주파 대역을 통과시키겠군" 하고 직관적으로 시스템의 운명을 예측할 수 있게 됩니다. 이것이 제어 공학과 신호 처리에서 Z-변환이 가지는 강력한 통찰입니다.

 

다음 글 예고

이번 편에서는 Z-변환의 정의와 ROC, 역 Z-변환, 그리고 단위 원과 DTFT의 관계를 살펴보았습니다. 특히 극점의 위치가 시스템의 안정성과 주파수 응답을 동시에 결정한다는 점을 확인하였습니다. Z-변환이 제공하는 2차원 지도 위에서 필터의 특성을 파악할 수 있는 기반을 다졌습니다.

다음 시간에는 [16. Z-변환 응용: 전달함수 H(z), 극점·영점과 필터 안정성] 편을 통해, Z-변환을 실제 필터 설계에 적용하는 방법을 더욱 깊이 다루겠습니다. 극점·영점 배치를 직접 설계하여 원하는 주파수 응답을 만들어 내는 과정을 Python 시뮬레이션과 함께 살펴보겠습니다.

 

📌 이전 연재 글 보기

반응형