@@ -180,6 +180,45 @@ def test_rfft_axis_3d(func, dtype, axis, order):
180180 _check (got , want , dtype )
181181
182182
183+ # norm="ortho" is unitary: it preserves the L2 norm. See gh-336.
184+
185+ _UNITARY_CASES = [
186+ ((16 , 18 ), (0 ,)), # gh-336
187+ ((16 , 18 ), (1 ,)),
188+ ((16 , 18 ), (0 , 1 )),
189+ (_SHAPE_3D , (0 ,)),
190+ (_SHAPE_3D , (1 ,)),
191+ (_SHAPE_3D , (2 ,)),
192+ (_SHAPE_3D , (0 , 2 )),
193+ (_SHAPE_3D , None ),
194+ ]
195+
196+ _UNITARY_RTOL = {"complex64" : 1e-4 , "complex128" : 1e-10 }
197+
198+
199+ @pytest .mark .parametrize ("func" , ["fftn" , "ifftn" ])
200+ @pytest .mark .parametrize ("dtype" , ["complex64" , "complex128" ])
201+ @pytest .mark .parametrize ("shape,axes" , _UNITARY_CASES )
202+ def test_ortho_preserves_norm (func , dtype , shape , axes ):
203+ x = _make (shape , dtype )
204+ x /= np .sqrt ((abs (x ) ** 2 ).sum ())
205+ got = getattr (mkl_fft , func )(x , axes = axes , norm = "ortho" )
206+ assert np .isclose (
207+ (abs (got ) ** 2 ).sum (), 1.0 , rtol = _UNITARY_RTOL [dtype ]
208+ ), f"norm not preserved: { (abs (got ) ** 2 ).sum ()} "
209+
210+
211+ @pytest .mark .parametrize ("shape,axes" , _UNITARY_CASES )
212+ def test_ortho_roundtrip_is_identity (shape , axes ):
213+ """Insensitive to gh-336: the fwd and bwd scales cancel. Guards only that
214+ the two compose to the identity.
215+ """
216+ x = _make (shape , "complex128" )
217+ fwd = mkl_fft .fftn (x , axes = axes , norm = "ortho" )
218+ got = mkl_fft .ifftn (fwd , axes = axes , norm = "ortho" )
219+ _check (got , x , "complex128" )
220+
221+
183222# ---------------------------------------------------------------------------
184223# norm interacts with the scale factor applied at dispatch time
185224# ---------------------------------------------------------------------------
0 commit comments