diff --git a/.github/workflows/test_package_macos-13.yaml b/.github/workflows/test_package_macos-13.yaml new file mode 100644 index 00000000..d21d8242 --- /dev/null +++ b/.github/workflows/test_package_macos-13.yaml @@ -0,0 +1,20 @@ +name: macos-13 +on: + workflow_call: + inputs: + python-version: + required: true + type: string + secrets: + TEST_PYPI_API_TOKEN: + required: true +jobs: + package: + uses: ./.github/workflows/test_package_main.yaml + with: + # macos-13: x86_64, macos-latest: arm + # See https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners + os: macos-13 + python-version: ${{ inputs.python-version }} + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_macos-13_python-3.10.yaml b/.github/workflows/test_package_macos-13_python-3.10.yaml new file mode 100644 index 00000000..16868531 --- /dev/null +++ b/.github/workflows/test_package_macos-13_python-3.10.yaml @@ -0,0 +1,15 @@ +name: macos-13/3.10 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_macos-13.yaml + with: + python-version: "3.10" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_macos-13_python-3.11.yaml b/.github/workflows/test_package_macos-13_python-3.11.yaml new file mode 100644 index 00000000..7a8e7076 --- /dev/null +++ b/.github/workflows/test_package_macos-13_python-3.11.yaml @@ -0,0 +1,15 @@ +name: macos-13/3.11 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_macos-13.yaml + with: + python-version: "3.11" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_macos-13_python-3.12.yaml b/.github/workflows/test_package_macos-13_python-3.12.yaml new file mode 100644 index 00000000..55e394b5 --- /dev/null +++ b/.github/workflows/test_package_macos-13_python-3.12.yaml @@ -0,0 +1,15 @@ +name: macos-13/3.12 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_macos-13.yaml + with: + python-version: "3.12" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_macos-13_python-3.8.yaml b/.github/workflows/test_package_macos-13_python-3.8.yaml new file mode 100644 index 00000000..7604df88 --- /dev/null +++ b/.github/workflows/test_package_macos-13_python-3.8.yaml @@ -0,0 +1,15 @@ +name: macos-13/3.8 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_macos-13.yaml + with: + python-version: "3.8" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_macos-13_python-3.9.yaml b/.github/workflows/test_package_macos-13_python-3.9.yaml new file mode 100644 index 00000000..8316f6cc --- /dev/null +++ b/.github/workflows/test_package_macos-13_python-3.9.yaml @@ -0,0 +1,15 @@ +name: macos-13/3.9 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_macos-13.yaml + with: + python-version: "3.9" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package.yaml b/.github/workflows/test_package_main.yaml similarity index 77% rename from .github/workflows/test_package.yaml rename to .github/workflows/test_package_main.yaml index 0cf31673..d44d6112 100644 --- a/.github/workflows/test_package.yaml +++ b/.github/workflows/test_package_main.yaml @@ -1,27 +1,27 @@ name: test package on: - push: - branches: - - main - pull_request: - branches: - - main - - develop + workflow_call: + inputs: + os: + required: true + type: string + python-version: + required: true + type: string + secrets: + TEST_PYPI_API_TOKEN: + required: true jobs: build: name: Run tests with pytest - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] - os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ inputs.os }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ inputs.python-version }} uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ inputs.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip @@ -42,13 +42,14 @@ jobs: run: | pytest -vvv -n 16 tests/regression/ - name: Upload coverage to codecov - if: matrix.python-version == '3.8' && matrix.os == 'ubuntu-latest' + if: inputs.python-version == '3.8' && inputs.os == 'ubuntu-latest' uses: codecov/codecov-action@v3 with: fail_ci_if_error: true lint: name: Run linters runs-on: ubuntu-latest + if: inputs.python-version == '3.8' && inputs.os == 'ubuntu-latest' steps: - name: Checkout uses: actions/checkout@v4 @@ -76,5 +77,5 @@ jobs: id-token: write secrets: TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && inputs.python-version == '3.9' && inputs.os == 'ubuntu-latest' uses: ./.github/workflows/upload_package.yaml diff --git a/.github/workflows/test_package_ubuntu-latest.yaml b/.github/workflows/test_package_ubuntu-latest.yaml new file mode 100644 index 00000000..a7a61559 --- /dev/null +++ b/.github/workflows/test_package_ubuntu-latest.yaml @@ -0,0 +1,18 @@ +name: macos-13 +on: + workflow_call: + inputs: + python-version: + required: true + type: string + secrets: + TEST_PYPI_API_TOKEN: + required: true +jobs: + package: + uses: ./.github/workflows/test_package_main.yaml + with: + os: ubuntu-latest + python-version: ${{ inputs.python-version }} + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_ubuntu-latest_python-3.10.yaml b/.github/workflows/test_package_ubuntu-latest_python-3.10.yaml new file mode 100644 index 00000000..03aa489f --- /dev/null +++ b/.github/workflows/test_package_ubuntu-latest_python-3.10.yaml @@ -0,0 +1,15 @@ +name: ubuntu-latest/3.10 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_ubuntu-latest.yaml + with: + python-version: "3.10" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_ubuntu-latest_python-3.11.yaml b/.github/workflows/test_package_ubuntu-latest_python-3.11.yaml new file mode 100644 index 00000000..3e1b71e6 --- /dev/null +++ b/.github/workflows/test_package_ubuntu-latest_python-3.11.yaml @@ -0,0 +1,15 @@ +name: ubuntu-latest/3.11 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_ubuntu-latest.yaml + with: + python-version: "3.11" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_ubuntu-latest_python-3.12.yaml b/.github/workflows/test_package_ubuntu-latest_python-3.12.yaml new file mode 100644 index 00000000..dd5c2b9a --- /dev/null +++ b/.github/workflows/test_package_ubuntu-latest_python-3.12.yaml @@ -0,0 +1,15 @@ +name: ubuntu-latest/3.12 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_ubuntu-latest.yaml + with: + python-version: "3.12" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_ubuntu-latest_python-3.8.yaml b/.github/workflows/test_package_ubuntu-latest_python-3.8.yaml new file mode 100644 index 00000000..d80f1fa4 --- /dev/null +++ b/.github/workflows/test_package_ubuntu-latest_python-3.8.yaml @@ -0,0 +1,15 @@ +name: ubuntu-latest/3.8 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_ubuntu-latest.yaml + with: + python-version: "3.8" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_ubuntu-latest_python-3.9.yaml b/.github/workflows/test_package_ubuntu-latest_python-3.9.yaml new file mode 100644 index 00000000..38e328b1 --- /dev/null +++ b/.github/workflows/test_package_ubuntu-latest_python-3.9.yaml @@ -0,0 +1,15 @@ +name: ubuntu-latest/3.9 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_ubuntu-latest.yaml + with: + python-version: "3.9" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_windows-latest.yaml b/.github/workflows/test_package_windows-latest.yaml new file mode 100644 index 00000000..02f06e4b --- /dev/null +++ b/.github/workflows/test_package_windows-latest.yaml @@ -0,0 +1,18 @@ +name: windows-latest +on: + workflow_call: + inputs: + python-version: + required: true + type: string + secrets: + TEST_PYPI_API_TOKEN: + required: true +jobs: + package: + uses: ./.github/workflows/test_package_main.yaml + with: + os: windows-latest + python-version: ${{ inputs.python-version }} + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_windows-latest_python-3.11.yaml b/.github/workflows/test_package_windows-latest_python-3.11.yaml new file mode 100644 index 00000000..6c819319 --- /dev/null +++ b/.github/workflows/test_package_windows-latest_python-3.11.yaml @@ -0,0 +1,15 @@ +name: windows-latest/3.11 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_windows-latest.yaml + with: + python-version: "3.11" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_windows-latest_python-3.12.yaml b/.github/workflows/test_package_windows-latest_python-3.12.yaml new file mode 100644 index 00000000..e2d5ebd8 --- /dev/null +++ b/.github/workflows/test_package_windows-latest_python-3.12.yaml @@ -0,0 +1,15 @@ +name: windows-latest/3.12 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_windows-latest.yaml + with: + python-version: "3.12" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_windows-latest_python-3.8.yaml b/.github/workflows/test_package_windows-latest_python-3.8.yaml new file mode 100644 index 00000000..140fcdb9 --- /dev/null +++ b/.github/workflows/test_package_windows-latest_python-3.8.yaml @@ -0,0 +1,15 @@ +name: windows-latest/3.8 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_windows-latest.yaml + with: + python-version: "3.8" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/.github/workflows/test_package_windows-latest_python-3.9.yaml b/.github/workflows/test_package_windows-latest_python-3.9.yaml new file mode 100644 index 00000000..d88e1f30 --- /dev/null +++ b/.github/workflows/test_package_windows-latest_python-3.9.yaml @@ -0,0 +1,15 @@ +name: windows-latest/3.9 +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + package: + uses: ./.github/workflows/test_package_windows-latest.yaml + with: + python-version: "3.9" + secrets: + TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} diff --git a/README.md b/README.md index 9ded84ec..ff4d8620 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ # ssspy [![Documentation Status](https://readthedocs.org/projects/sound-source-separation-python/badge/?version=latest)](https://sound-source-separation-python.readthedocs.io/en/latest/?badge=latest) -[![tests](https://github.com/tky823/ssspy/actions/workflows/test_package.yaml/badge.svg)](https://github.com/tky823/ssspy/actions/workflows/test_package.yaml) -[![codecov](https://codecov.io/gh/tky823/ssspy/branch/main/graph/badge.svg?token=IZ89MTV64G)](https://codecov.io/gh/tky823/ssspy) +[![codecov](https://codecov.io/gh/tky823/ssspy/branch/main/graph/badge.svg)](https://codecov.io/gh/tky823/ssspy) [![Open in Spaces](https://huggingface.co/datasets/huggingface/badges/resolve/main/open-in-hf-spaces-sm.svg)](https://tky823-ssspy-demo.hf.space/) A Python toolkit for sound source separation. +## Build Status +[![ubuntu-latest/3.9](https://github.com/tky823/ssspy/actions/workflows/test_package.yaml/badge.svg)](https://github.com/tky823/ssspy/actions/workflows/test_package.yaml) + + ## Installation You can install by pip. ```shell diff --git a/ssspy/bss/_update_spatial_model.py b/ssspy/bss/_update_spatial_model.py index 90d4dc12..01fde1be 100644 --- a/ssspy/bss/_update_spatial_model.py +++ b/ssspy/bss/_update_spatial_model.py @@ -3,7 +3,9 @@ import numpy as np -from ..linalg import eigh2, inv2 +from ..linalg._solve import solve +from ..linalg.eigh import eigh2 +from ..linalg.inv import inv2 from ..linalg.lqpqm import lqpqm2 from ..special.flooring import identity, max_flooring from ..special.psd import to_psd @@ -64,7 +66,7 @@ def update_by_ip1( e_n = E[:, src_idx, :] # (n_bins, n_n_channels) WU = W @ U_n - w_n = np.linalg.solve(WU, e_n) # (n_bins, n_channels) + w_n = solve(WU, e_n) # (n_bins, n_channels) wUw = w_n[:, np.newaxis, :].conj() @ U_n @ w_n[:, :, np.newaxis] wUw = np.real(wUw[..., 0]) wUw = np.maximum(wUw, 0) @@ -358,8 +360,8 @@ def update_by_ip2_one_pair( WU_m = W @ U_m WU_n = W @ U_n - P_m = np.linalg.solve(WU_m, E_mn) - P_n = np.linalg.solve(WU_n, E_mn) + P_m = solve(WU_m, E_mn) + P_n = solve(WU_n, E_mn) PUP_m = P_m.transpose(0, 2, 1).conj() @ U_m @ P_m PUP_n = P_n.transpose(0, 2, 1).conj() @ U_n @ P_n @@ -457,7 +459,7 @@ def update_by_ipa( C_n = d_n @ E_n d_n = d_n[:, :, source_idx] - Cd_n = np.linalg.solve(C_n, d_n) + Cd_n = solve(C_n, d_n) dCd_n = np.sum(d_n.conj() * Cd_n, axis=-1) dCd_n = np.real(dCd_n) eUe_n = U_tilde_n_inverse[:, source_idx, source_idx] @@ -490,7 +492,7 @@ def update_by_ipa( Eq_n = q_n.conj() @ E_n.transpose(1, 0) q_tilde_n = e_n.transpose(1, 0) - Eq_n - Uq_n = np.linalg.solve(U_tilde_n, q_tilde_n) + Uq_n = solve(U_tilde_n, q_tilde_n) qUq_n = np.sum(q_tilde_n.conj() * Uq_n, axis=-1, keepdims=True) qUq_n = np.real(qUq_n) @@ -577,8 +579,8 @@ def _is_zero(x: np.ndarray) -> np.ndarray: gamma_in = np.sum(pad_mask_i[:, na] * RXY_in[..., 0], axis=1) WU_in = W[:, neighbor_idx, :, :] @ U_in - eta_in = np.linalg.solve(WU_in, e_n) - eta_hat_in = np.linalg.solve(U_in, gamma_in) + eta_in = solve(WU_in, e_n) + eta_hat_in = solve(U_in, gamma_in) eta_U_in = eta_in[:, na, :].conj() @ U_in xi_in = eta_U_in @ eta_in[:, :, na] diff --git a/ssspy/bss/admmbss.py b/ssspy/bss/admmbss.py index 0383bded..70f0786c 100644 --- a/ssspy/bss/admmbss.py +++ b/ssspy/bss/admmbss.py @@ -4,6 +4,7 @@ import numpy as np from ..linalg import prox +from ..linalg._solve import solve from .proxbss import ProxBSSBase EPS = 1e-10 @@ -232,7 +233,7 @@ def update_once(self) -> None: VY_tilde = np.sum(V_tilde - Y_tilde, axis=0) XVY_tilde = X.transpose(1, 0, 2).conj() @ VY_tilde.transpose(1, 2, 0) - W = np.linalg.solve(n_penalties * XX + E, VY + XVY_tilde.transpose(0, 2, 1)) + W = solve(n_penalties * XX + E, VY + XVY_tilde.transpose(0, 2, 1)) XW = self.separate(X, demix_filter=W) U = alpha * W + (1 - alpha) * V @@ -426,7 +427,7 @@ def update_once(self) -> None: VY_tilde = V_tilde - Y_tilde XVY_tilde = X.transpose(1, 0, 2).conj() @ VY_tilde.transpose(1, 2, 0) - W = np.linalg.solve(XX + E, VY + XVY_tilde.transpose(0, 2, 1)) + W = solve(XX + E, VY + XVY_tilde.transpose(0, 2, 1)) XW = self.separate(X, demix_filter=W) U = alpha * W + (1 - alpha) * V diff --git a/ssspy/bss/mnmf.py b/ssspy/bss/mnmf.py index 7dd5bf0c..adf9dcf9 100644 --- a/ssspy/bss/mnmf.py +++ b/ssspy/bss/mnmf.py @@ -3,6 +3,7 @@ import numpy as np +from ..linalg._solve import solve from ..linalg.mean import gmeanmh from ..special.flooring import identity, max_flooring from ..special.psd import to_psd @@ -753,7 +754,7 @@ def separate(self, input: np.ndarray) -> np.ndarray: R = np.sum(R_n, axis=0) R = to_psd(R, flooring_fn=self.flooring_fn) R = np.tile(R, reps=(n_sources, 1, 1, 1, 1)) - W_Hermite = np.linalg.solve(R, R_n) + W_Hermite = solve(R, R_n) W = W_Hermite.transpose(0, 1, 2, 4, 3).conj() W_ref = W[:, :, :, reference_id, :] W_ref = W_ref.transpose(0, 3, 1, 2) @@ -777,7 +778,7 @@ def compute_loss(self) -> float: R = self.reconstruct_mnmf(T, V, H) R = to_psd(R, flooring_fn=self.flooring_fn) - XXR_inv = np.linalg.solve(R, XX) # Hermitian transpose of XX @ np.linalg.inv(R) + XXR_inv = solve(R, XX) # Hermitian transpose of XX @ np.linalg.inv(R) trace = np.trace(XXR_inv, axis1=-2, axis2=-1) trace = np.real(trace) logdet = self.compute_logdet(R) @@ -857,10 +858,10 @@ def update_basis( def _compute_traces( target: np.ndarray, reconstructed: np.ndarray, spatial: np.ndarray ) -> np.ndarray: - RXX = np.linalg.solve(reconstructed, target) + RXX = solve(reconstructed, target) R = np.tile(reconstructed, reps=(n_sources, 1, 1, 1, 1)) H = np.tile(spatial[:, :, na, :, :], reps=(1, 1, n_frames, 1, 1)) - RH = np.linalg.solve(R, H) + RH = solve(R, H) trace_RXXRH = np.trace(RXX @ RH, axis1=-2, axis2=-1) trace_RXXRH = np.real(trace_RXXRH) @@ -924,10 +925,10 @@ def update_activation( def _compute_traces( target: np.ndarray, reconstructed: np.ndarray, spatial: np.ndarray ) -> np.ndarray: - RXX = np.linalg.solve(reconstructed, target) + RXX = solve(reconstructed, target) R = np.tile(reconstructed, reps=(n_sources, 1, 1, 1, 1)) H = np.tile(spatial[:, :, na, :, :], reps=(1, 1, n_frames, 1, 1)) - RH = np.linalg.solve(R, H) + RH = solve(R, H) trace_RXXRH = np.trace(RXX @ RH, axis1=-2, axis2=-1) trace_RXXRH = np.real(trace_RXXRH) @@ -1039,10 +1040,10 @@ def update_latent( def _compute_traces( target: np.ndarray, reconstructed: np.ndarray, spatial: np.ndarray ) -> np.ndarray: - RXX = np.linalg.solve(reconstructed, target) + RXX = solve(reconstructed, target) R = np.tile(reconstructed, reps=(n_sources, 1, 1, 1, 1)) H = np.tile(spatial[:, :, na, :, :], reps=(1, 1, n_frames, 1, 1)) - RH = np.linalg.solve(R, H) + RH = solve(R, H) trace_RXXRH = np.trace(RXX @ RH, axis1=-2, axis2=-1) trace_RXXRH = np.real(trace_RXXRH) @@ -1207,7 +1208,7 @@ def separate(self, input: np.ndarray) -> np.ndarray: R = np.sum(R_n, axis=0) R = to_psd(R, flooring_fn=self.flooring_fn) R = np.tile(R, reps=(n_sources, 1, 1, 1, 1)) - W_Hermite = np.linalg.solve(R, R_n) + W_Hermite = solve(R, R_n) W = W_Hermite.transpose(0, 1, 2, 4, 3).conj() W_ref = W[:, :, :, reference_id, :] W_ref = W_ref.transpose(0, 3, 1, 2) diff --git a/ssspy/linalg/__init__.py b/ssspy/linalg/__init__.py index c21dcf23..85ea0405 100644 --- a/ssspy/linalg/__init__.py +++ b/ssspy/linalg/__init__.py @@ -1,3 +1,4 @@ +from ._solve import solve from .cubic import cbrt from .eigh import eigh, eigh2 from .inv import inv2 @@ -18,4 +19,5 @@ "gmeanmh", "solve_cubic", "lqpqm2", + "solve", ] diff --git a/ssspy/linalg/_solve.py b/ssspy/linalg/_solve.py new file mode 100644 index 00000000..7ffde383 --- /dev/null +++ b/ssspy/linalg/_solve.py @@ -0,0 +1,21 @@ +import numpy as np +from packaging import version + +np_version = np.__version__ + +IS_NUMPY_GE_2 = version.parse(np.__version__) >= version.parse("2") + + +def solve(a: np.ndarray, b: np.ndarray) -> np.ndarray: + requires_new_axis = IS_NUMPY_GE_2 and a.ndim == b.ndim + 1 + + if requires_new_axis: + b = b[..., np.newaxis] + + x = np.linalg.solve(a, b) + + if requires_new_axis: + x = x[..., 0] + b = b[..., 0] + + return x