OpenMC/tests/unit_tests/test_vectfit.py
azim-givron 360ec24b41
Implement vector fitting to replace external vectfit package (#3493)
Co-authored-by: azim_givron <a.givron@naarea.fr>
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
Co-authored-by: GuySten <guyste@post.bgu.ac.il>
Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com>
2026-02-11 10:00:18 -06:00

224 lines
8.1 KiB
Python

"""
Initially from Jingang Liang: https://github.com/mit-crpg/vectfit.git
"""
import numpy as np
import pytest
from openmc.data.vectfit import evaluate, vectfit
@pytest.fixture
def ref_poles():
"""Reference poles for real-pole test."""
return np.array(
[
9.709261771920490e02 + 0.0j,
-1.120960794075339e03 + 0.0j,
1.923889557426567e00 + 7.543700246109742e01j,
1.923889557426567e00 - 7.543700246109742e01j,
1.159741300380281e02 + 3.595650922556496e-02j,
1.159741300380281e02 - 3.595650922556496e-02j,
1.546932165729394e02 + 8.728391144940301e-02j,
1.546932165729394e02 - 8.728391144940301e-02j,
2.280349190818197e02 + 2.814037559718684e-01j,
2.280349190818197e02 - 2.814037559718684e-01j,
2.313004772627853e02 + 3.004628477692201e-01j,
2.313004772627853e02 - 3.004628477692201e-01j,
2.787470098364861e02 + 3.414179169920170e-01j,
2.787470098364861e02 - 3.414179169920170e-01j,
3.570711338764254e02 + 4.485587371149193e-01j,
3.570711338764254e02 - 4.485587371149193e-01j,
4.701059001346060e02 + 6.598089307174224e-01j,
4.701059001346060e02 - 6.598089307174224e-01j,
7.275819506342254e02 + 1.189678974845038e03j,
7.275819506342254e02 - 1.189678974845038e03j,
]
)
@pytest.fixture
def ref_residues():
"""Reference residues for real-pole test."""
return np.array(
[
[
-3.269879776751686e07 + 0.0j,
1.131087935798761e09 + 0.0j,
1.634151281869857e04 + 2.251103589277891e05j,
1.634151281869857e04 - 2.251103589277891e05j,
3.281792303833561e03 - 1.756079516325274e04j,
3.281792303833561e03 + 1.756079516325274e04j,
1.110800880243503e04 - 4.324813594540043e04j,
1.110800880243503e04 + 4.324813594540043e04j,
8.812700704117636e04 - 2.256520243571103e05j,
8.812700704117636e04 + 2.256520243571103e05j,
5.842090495551535e04 - 1.442159380741478e05j,
5.842090495551535e04 + 1.442159380741478e05j,
1.339410514130921e05 - 2.640767909713812e05j,
1.339410514130921e05 + 2.640767909713812e05j,
2.211245633333130e05 - 3.222447758311512e05j,
2.211245633333130e05 + 3.222447758311512e05j,
4.124430059785149e05 - 4.076023108323907e05j,
4.124430059785149e05 + 4.076023108323907e05j,
1.607378314999252e09 - 1.401163320110452e08j,
1.607378314999252e09 + 1.401163320110452e08j,
]
]
)
@pytest.fixture
def vector_test_data():
"""Simple 2-signal test with known poles and residues."""
Ns = 101
s = np.linspace(3.0, 7.0, Ns)
poles = [5.0 + 0.1j, 5.0 - 0.1j]
residues = [[0.5 - 11.0j, 0.5 + 11.0j], [1.5 - 20.0j, 1.5 + 20.0j]]
f = np.zeros((2, Ns))
for i in range(2):
f[i, :] = np.real(
residues[i][0] / (s - poles[0]) + residues[i][1] / (s - poles[1])
)
weight = 1.0 / f
init_poles = [3.5 + 0.035j, 3.5 - 0.035j]
return s, poles, residues, f, weight, init_poles
@pytest.fixture
def poly_test_data():
"""Test data with rational function plus polynomial terms."""
Ns = 201
s = np.linspace(0.0, 5.0, Ns)
poles = [-20.0 + 30.0j, -20.0 - 30.0j]
residues = [[5.0 + 10.0j, 5.0 - 10.0j]]
polys = [[1.0, 2.0, 0.3]]
f = evaluate(s, poles, residues, polys)
weight = 1.0 / f
init_poles = [2.5 + 0.025j, 2.5 - 0.025j]
return s, poles, residues, polys, f, weight, init_poles
@pytest.fixture
def real_poles_data(ref_poles, ref_residues):
"""Large-scale signal using complex and real poles."""
Ns = 2000
s = np.linspace(1.0e-2, 5.0e3, Ns)
f = np.zeros((1, Ns))
for p, r in zip(ref_poles, ref_residues[0]):
f[0] += (r / (s - p)).real
weight = 1.0 / f
poles = np.linspace(1.1e-2, 4.8e3, 10)
poles = poles + poles * 0.01j
poles = np.sort(np.append(poles, np.conj(poles)))
return s, f, weight, poles
@pytest.fixture
def large_test_data():
"""Stress test data with thousands of poles and samples."""
Ns = 3000
N = 200
s = np.linspace(1.0e-2, 5.0e3, Ns)
poles = np.linspace(1.1e-2, 4.8e3, N // 2) + 0.01j * np.linspace(
1.1e-2, 4.8e3, N // 2
)
poles = np.sort(np.append(poles, np.conj(poles)))
residues = np.linspace(1e2, 1e6, N // 2) + 0.5j * np.linspace(1e2, 1e6, N // 2)
residues = np.sort(np.append(residues, np.conj(residues))).reshape((1, N))
f = np.zeros((1, Ns))
for p, r in zip(poles, residues[0]):
f[0] += (r / (s - p)).real
weight = 1.0 / f
init_poles = np.linspace(1.2e-2, 4.7e3, N // 2) + 0.01j * np.linspace(
1.2e-2, 4.7e3, N // 2
)
init_poles = np.sort(np.append(init_poles, np.conj(init_poles)))
return s, f, weight, init_poles
@pytest.fixture
def eval_test_data():
"""Reference data for evaluating rational + polynomial models."""
Ns = 101
s = np.linspace(-5.0, 5.0, Ns)
poles = [-2.0 + 30.0j, -2.0 - 30.0j]
residues = [5.0 + 10.0j, 5.0 - 10.0j]
polys = [1.0, 2.0, 0.3]
return s, poles, residues, polys
def test_vector(vector_test_data):
"""Test vectfit with vector samples and simple poles.
It is expected to get exact results with one iteration.
"""
s, expected_poles, expected_residues, f, weight, init_poles = vector_test_data
poles, residues, _, fit, _ = vectfit(f, s, init_poles, weight)
assert np.allclose(
np.sort_complex(poles), np.sort_complex(expected_poles), rtol=1e-7
)
assert np.allclose(f, evaluate(s, poles, residues), rtol=1e-7)
assert np.allclose(f, fit, rtol=1e-5)
def test_poly(poly_test_data):
"""Test vectfit with polynomials."""
s, expected_poles, expected_residues, expected_polys, f, weight, init_poles = (
poly_test_data
)
poles, residues, cf, fit, _ = vectfit(f, s, init_poles, weight, n_polys=3)
poles, residues, cf, fit, _ = vectfit(f, s, poles, weight, n_polys=3)
assert np.allclose(
np.sort_complex(poles), np.sort_complex(expected_poles), rtol=1e-5
)
assert np.allclose(f, evaluate(s, poles, residues, cf), rtol=1e-5)
assert np.allclose(cf, expected_polys, rtol=1e-5)
assert np.allclose(f, fit, rtol=1e-4)
def test_real_poles(real_poles_data, ref_poles, ref_residues):
"""Test vectfit with more poles including real poles"""
s, f, weight, poles = real_poles_data
for _ in range(6):
poles, residues, _, fit, _ = vectfit(f, s, poles, weight)
assert np.allclose(
np.sort_complex(poles), np.sort_complex(ref_poles), rtol=1e-5, atol=1e-8
)
assert np.allclose(f, evaluate(s, poles, residues), rtol=1e-4)
assert np.allclose(f, fit, rtol=1e-3)
def test_large(large_test_data):
"""Test vectfit with a large set of poles and samples"""
s, f, weight, init_poles = large_test_data
poles_fit, residues_fit, _, f_fit, _ = vectfit(f, s, init_poles, weight)
assert np.allclose(f, f_fit, rtol=1e-3)
def test_evaluate(eval_test_data):
"""Test evaluate function"""
s, poles, residues, polys = eval_test_data
# Single signal, no polynomial
f_ref = np.real(residues[0] / (s - poles[0]) + residues[1] / (s - poles[1]))
f = evaluate(s, poles, residues)
assert np.allclose(f[0], f_ref)
# Single signal, with polynomial
for n, c in enumerate(polys):
f_ref += c * np.power(s, n)
f = evaluate(s, poles, residues, polys)
assert np.allclose(f[0], f_ref)
# Multi-signal, multi-residue, multi-poly
poles = [5.0 + 0.1j, 5.0 - 0.1j]
residues = [[0.5 - 11.0j, 0.5 + 11.0j], [1.5 - 20.0j, 1.5 + 20.0j]]
polys = [[1.0, 2.0, 0.3], [4.0, -2.0, -10.0]]
f_ref = np.zeros((2, len(s)))
for i in range(2):
f_ref[i, :] = np.real(
residues[i][0] / (s - poles[0]) + residues[i][1] / (s - poles[1])
)
for n, c in enumerate(polys[i]):
f_ref[i, :] += c * np.power(s, n)
f = evaluate(s, poles, residues, polys)
assert np.allclose(f, f_ref)