22
33import numpy as np
44
5+ from CodeEntropy .axes import AxesManager
56from CodeEntropy .levels import LevelManager
67from CodeEntropy .mda_universe_operations import UniverseOperations
78from tests .test_CodeEntropy .test_base import BaseTestCase
@@ -383,48 +384,47 @@ def test_get_matrices_united_atom_customised_axes(self):
383384 axes .get_UA_axes .assert_called_once ()
384385 assert axes .get_residue_axes .call_count == 0
385386
386- def test_get_matrices_non_customised_axes_path (self ):
387+ def test_get_matrices_non_customised_axes_path_atomic (self ):
387388 """
388- Test that: customised_axes=False triggers the else axes path.
389- Covers:
390- trans_axes = data_container.atoms.principal_axes()
391- rot_axes = real(bead. principal_axes())
392- eigenvalues, _ = np.linalg.eig( bead.moment_of_inertia() )
393- moment_of_inertia sorted(...)
394- center = bead.center_of_mass()
389+ Tests that ` customised_axes=False` triggers the non-customised axes path.
390+
391+ Verifies that:
392+ - translational axes are taken from `data_container.atoms. principal_axes()`
393+ - rotational axes are taken from ` bead.principal_axes()` (real-valued )
394+ - bead moment of inertia and center of mass are queried
395+ - force and torque matrices are assembled with size (3N, 3N) for N beads
395396 """
396397 universe_operations = UniverseOperations ()
397398 level_manager = LevelManager (universe_operations )
398399
399- bead1 = MagicMock ()
400- bead2 = MagicMock ()
401-
402- bead1 .principal_axes .return_value = np .eye (3 )
403- bead2 .principal_axes .return_value = np .eye (3 )
404-
400+ bead1 , bead2 = MagicMock (), MagicMock ()
401+ bead1 .principal_axes .return_value = np .eye (3 ) * (1 + 2j )
402+ bead2 .principal_axes .return_value = np .eye (3 ) * (1 + 2j )
405403 bead1 .center_of_mass .return_value = np .zeros (3 )
406404 bead2 .center_of_mass .return_value = np .zeros (3 )
407-
408405 bead1 .moment_of_inertia .return_value = np .eye (3 )
409406 bead2 .moment_of_inertia .return_value = np .eye (3 )
410407
411408 level_manager .get_beads = MagicMock (return_value = [bead1 , bead2 ])
412-
413409 level_manager .get_weighted_forces = MagicMock (
414410 return_value = np .array ([1.0 , 2.0 , 3.0 ])
415411 )
416412 level_manager .get_weighted_torques = MagicMock (
417413 return_value = np .array ([0.5 , 1.5 , 2.5 ])
418414 )
419- level_manager .create_submatrix = MagicMock (return_value = np .identity (3 ))
415+ level_manager .create_submatrix = MagicMock (return_value = np .eye (3 ))
420416
421417 data_container = MagicMock ()
422418 data_container .atoms = MagicMock ()
423419 data_container .atoms .principal_axes .return_value = np .eye (3 )
424420
425- with patch ("CodeEntropy.levels.np.linalg.eig" ) as eig_mock :
426- eig_mock .return_value = (np .array ([3.0 , 2.0 , 1.0 ]), None )
427-
421+ with (
422+ patch ("CodeEntropy.levels.make_whole" , autospec = True ),
423+ patch (
424+ "CodeEntropy.levels.np.linalg.eig" ,
425+ return_value = (np .array ([1.0 , 3.0 , 2.0 ]), None ),
426+ ),
427+ ):
428428 force_matrix , torque_matrix = level_manager .get_matrices (
429429 data_container = data_container ,
430430 level = "polymer" ,
@@ -435,15 +435,17 @@ def test_get_matrices_non_customised_axes_path(self):
435435 customised_axes = False ,
436436 )
437437
438+ data_container .atoms .principal_axes .assert_called ()
439+ bead1 .principal_axes .assert_called ()
440+ bead2 .principal_axes .assert_called ()
441+ bead1 .center_of_mass .assert_called ()
442+ bead2 .center_of_mass .assert_called ()
443+ bead1 .moment_of_inertia .assert_called ()
444+ bead2 .moment_of_inertia .assert_called ()
445+
438446 assert force_matrix .shape == (6 , 6 )
439447 assert torque_matrix .shape == (6 , 6 )
440448
441- data_container .atoms .principal_axes .assert_called ()
442- assert bead1 .principal_axes .called and bead2 .principal_axes .called
443- assert bead1 .center_of_mass .called and bead2 .center_of_mass .called
444- assert bead1 .moment_of_inertia .called and bead2 .moment_of_inertia .called
445- assert eig_mock .call_count == 2
446-
447449 def test_get_matrices_accepts_existing_same_shape (self ):
448450 """
449451 Test that: if force_matrix and torque_matrix are provided with correct shape,
@@ -558,43 +560,54 @@ def test_get_combined_forcetorque_matrices_residue_customised_init(self):
558560 def test_get_combined_forcetorque_matrices_noncustomised_axes_path (self ):
559561 """
560562 Test that: customised_axes=False forces else-path:
561- trans_axes = data_container.atoms.principal_axes()
562- rot_axes = real(bead.principal_axes())
563- eig(bead.moment_of_inertia()) called
564- center_of_mass called
563+ - make_whole(data_container.atoms) and make_whole(bead) called
564+ - trans_axes = data_container.atoms.principal_axes()
565+ - rot_axes, moment_of_inertia = AxesManager.get_vanilla_axes(bead)
566+ - center = bead.center_of_mass(unwrap=True)
567+ - FT block matrix assembled via create_FTsubmatrix and np.block
565568 """
566569 universe_operations = UniverseOperations ()
567570 level_manager = LevelManager (universe_operations )
568571
569- bead1 = MagicMock ()
570- bead2 = MagicMock ()
571-
572- bead1 .principal_axes .return_value = np .eye (3 )
573- bead2 .principal_axes .return_value = np .eye (3 )
572+ bead1 = MagicMock (name = "bead1" )
573+ bead2 = MagicMock (name = "bead2" )
574+ beads = [bead1 , bead2 ]
574575
575- bead1 .moment_of_inertia .return_value = np .eye (3 )
576- bead2 .moment_of_inertia .return_value = np .eye (3 )
577-
578- bead1 .center_of_mass .return_value = np .zeros (3 )
579- bead2 .center_of_mass .return_value = np .zeros (3 )
576+ level_manager .get_beads = MagicMock (return_value = beads )
580577
581- level_manager .get_beads = MagicMock (return_value = [bead1 , bead2 ])
578+ data_container = MagicMock (name = "data_container" )
579+ data_container .atoms = MagicMock (name = "atoms" )
580+ data_container .atoms .principal_axes .return_value = np .eye (3 )
582581
582+ # Forces/torques are 3-vectors -> concatenated to length 6
583583 level_manager .get_weighted_forces = MagicMock (
584- return_value = np .array ([1.0 , 2.0 , 3.0 ])
584+ side_effect = [
585+ np .array ([1.0 , 2.0 , 3.0 ]),
586+ np .array ([1.1 , 2.1 , 3.1 ]),
587+ ]
585588 )
586589 level_manager .get_weighted_torques = MagicMock (
587- return_value = np .array ([4.0 , 5.0 , 6.0 ])
590+ side_effect = [
591+ np .array ([4.0 , 5.0 , 6.0 ]),
592+ np .array ([4.1 , 5.1 , 6.1 ]),
593+ ]
588594 )
589595
590596 level_manager .create_FTsubmatrix = MagicMock (return_value = np .identity (6 ))
591597
592- data_container = MagicMock ()
593- data_container .atoms = MagicMock ()
594- data_container .atoms .principal_axes .return_value = np .eye (3 )
598+ rot_axes_expected = np .eye (3 )
599+ moi_expected = np .array ([3.0 , 2.0 , 1.0 ])
595600
596- with patch ("CodeEntropy.levels.np.linalg.eig" ) as eig_mock :
597- eig_mock .return_value = (np .array ([3.0 , 2.0 , 1.0 ]), None )
601+ with (
602+ patch ("CodeEntropy.levels.make_whole" , autospec = True ) as mw_mock ,
603+ patch (
604+ "CodeEntropy.axes.AxesManager.get_vanilla_axes" ,
605+ autospec = True ,
606+ return_value = (rot_axes_expected , moi_expected ),
607+ ) as vanilla_mock ,
608+ ):
609+ bead1 .center_of_mass .return_value = np .zeros (3 )
610+ bead2 .center_of_mass .return_value = np .zeros (3 )
598611
599612 ft_matrix = level_manager .get_combined_forcetorque_matrices (
600613 data_container = data_container ,
@@ -605,13 +618,20 @@ def test_get_combined_forcetorque_matrices_noncustomised_axes_path(self):
605618 customised_axes = False ,
606619 )
607620
608- assert ft_matrix .shape == (12 , 12 )
609-
610621 data_container .atoms .principal_axes .assert_called ()
611- assert bead1 .principal_axes .called and bead2 .principal_axes .called
612- assert bead1 .moment_of_inertia .called and bead2 .moment_of_inertia .called
613- assert bead1 .center_of_mass .called and bead2 .center_of_mass .called
614- assert eig_mock .call_count == 2
622+ bead1 .center_of_mass .assert_called_with (unwrap = True )
623+ bead2 .center_of_mass .assert_called_with (unwrap = True )
624+
625+ assert vanilla_mock .call_count == 2 # once per bead
626+
627+ # make_whole is called twice per bead: on data_container.atoms and on bead
628+ assert mw_mock .call_count == 4
629+ mw_mock .assert_any_call (data_container .atoms )
630+ mw_mock .assert_any_call (bead1 )
631+ mw_mock .assert_any_call (bead2 )
632+
633+ # result shape: (6N, 6N) with N=2
634+ assert ft_matrix .shape == (12 , 12 )
615635
616636 def test_get_combined_forcetorque_matrices_shape_mismatch_raises (self ):
617637 """
@@ -924,61 +944,75 @@ def test_get_weighted_forces_negative_mass_raises_value_error(self):
924944
925945 def test_get_weighted_torques_weighted_torque_basic (self ):
926946 """
927- Test basic torque calculation with non-zero moment of inertia and torques.
947+ Test basic weighted torque calculation for a single-atom bead.
948+
949+ Setup:
950+ r = [1, 0, 0], F = [0, 1, 0] => r x F = [0, 0, 1]
951+ With force_partitioning=0.5, rot_axes=I, MOI=[1,1,1],
952+ expected weighted torque is [0, 0, 0.5].
928953 """
929954 universe_operations = UniverseOperations ()
930955 level_manager = LevelManager (universe_operations )
956+ axes_manager = AxesManager ()
931957
932- # Bead with one "atom"
933958 bead = MagicMock ()
934- bead .positions = np .array ([[1.0 , 0.0 , 0.0 ]]) # r
935- bead .forces = np .array ([[0.0 , 1.0 , 0.0 ]]) # F
959+ bead .positions = np .array ([[1.0 , 0.0 , 0.0 ]])
960+ bead .forces = np .array ([[0.0 , 1.0 , 0.0 ]])
961+ bead .dimensions = np .array ([10.0 , 10.0 , 10.0 ])
936962
937- rot_axes = np .identity (3 )
938- center = np .array ([ 0.0 , 0.0 , 0.0 ] )
963+ rot_axes = np .eye (3 )
964+ center = np .zeros ( 3 )
939965 force_partitioning = 0.5
940966 moment_of_inertia = np .array ([1.0 , 1.0 , 1.0 ])
941967
942- result = level_manager .get_weighted_torques (
943- bead = bead ,
944- rot_axes = rot_axes ,
945- center = center ,
946- force_partitioning = force_partitioning ,
947- moment_of_inertia = moment_of_inertia ,
948- )
968+ with patch .object (
969+ AxesManager , "get_vector" , return_value = bead .positions - center
970+ ) as gv_mock :
971+ result = level_manager .get_weighted_torques (
972+ bead = bead ,
973+ rot_axes = rot_axes ,
974+ center = center ,
975+ force_partitioning = force_partitioning ,
976+ moment_of_inertia = moment_of_inertia ,
977+ axes_manager = axes_manager ,
978+ )
979+
980+ gv_mock .assert_called ()
949981
950982 expected = np .array ([0.0 , 0.0 , 0.5 ])
951- np .testing .assert_allclose (result , expected , rtol = 0 , atol = 1e-12 )
983+ np .testing .assert_allclose (result , expected )
952984
953985 def test_get_weighted_torques_zero_torque_skips_division (self ):
954986 """
955987 Test that zero torque components skip division and remain zero.
956988 """
957989 universe_operations = UniverseOperations ()
958990 level_manager = LevelManager (universe_operations )
991+ axes_manager = AxesManager ()
959992
960993 bead = MagicMock ()
961- # All zeros => r x F = 0
962994 bead .positions = np .array ([[0.0 , 0.0 , 0.0 ]])
963995 bead .forces = np .array ([[0.0 , 0.0 , 0.0 ]])
996+ bead .dimensions = np .array ([10.0 , 10.0 , 10.0 ])
964997
965998 rot_axes = np .identity (3 )
966999 center = np .array ([0.0 , 0.0 , 0.0 ])
9671000 force_partitioning = 0.5
968-
969- # Use non-zero MOI so that "skip division" is only due to zero torque
9701001 moment_of_inertia = np .array ([1.0 , 2.0 , 3.0 ])
9711002
972- result = level_manager .get_weighted_torques (
973- bead = bead ,
974- rot_axes = rot_axes ,
975- center = center ,
976- force_partitioning = force_partitioning ,
977- moment_of_inertia = moment_of_inertia ,
978- )
1003+ with patch .object (
1004+ AxesManager , "get_vector" , return_value = bead .positions - center
1005+ ):
1006+ result = level_manager .get_weighted_torques (
1007+ bead = bead ,
1008+ rot_axes = rot_axes ,
1009+ center = center ,
1010+ force_partitioning = force_partitioning ,
1011+ moment_of_inertia = moment_of_inertia ,
1012+ axes_manager = axes_manager ,
1013+ )
9791014
980- expected = np .zeros (3 )
981- np .testing .assert_array_equal (result , expected )
1015+ np .testing .assert_array_equal (result , np .zeros (3 ))
9821016
9831017 def test_get_weighted_torques_zero_moi (self ):
9841018 """
@@ -987,31 +1021,31 @@ def test_get_weighted_torques_zero_moi(self):
9871021 """
9881022 universe_operations = UniverseOperations ()
9891023 level_manager = LevelManager (universe_operations )
1024+ axes_manager = AxesManager ()
9901025
9911026 bead = MagicMock ()
992- # r = (1,0,0), F = (0,1,0) => torque = (0,0,1)
9931027 bead .positions = np .array ([[1.0 , 0.0 , 0.0 ]])
9941028 bead .forces = np .array ([[0.0 , 1.0 , 0.0 ]])
1029+ bead .dimensions = np .array ([10.0 , 10.0 , 10.0 ])
9951030
9961031 rot_axes = np .identity (3 )
9971032 center = np .array ([0.0 , 0.0 , 0.0 ])
9981033 force_partitioning = 0.5
999-
1000- # MOI is zero in z dimension (index 2)
10011034 moment_of_inertia = np .array ([1.0 , 1.0 , 0.0 ])
10021035
1003- torque = level_manager .get_weighted_torques (
1004- bead = bead ,
1005- rot_axes = rot_axes ,
1006- center = center ,
1007- force_partitioning = force_partitioning ,
1008- moment_of_inertia = moment_of_inertia ,
1009- )
1036+ with patch .object (
1037+ AxesManager , "get_vector" , return_value = bead .positions - center
1038+ ):
1039+ torque = level_manager .get_weighted_torques (
1040+ bead = bead ,
1041+ rot_axes = rot_axes ,
1042+ center = center ,
1043+ force_partitioning = force_partitioning ,
1044+ moment_of_inertia = moment_of_inertia ,
1045+ axes_manager = axes_manager ,
1046+ )
10101047
1011- # x and y torques are zero; z torque is non-zero
1012- # but MOI_z==0 => weighted z should be 0
1013- expected = np .array ([0.0 , 0.0 , 0.0 ])
1014- np .testing .assert_array_equal (torque , expected )
1048+ np .testing .assert_array_equal (torque , np .zeros (3 ))
10151049
10161050 def test_get_weighted_torques_negative_moi_sets_zero (self ):
10171051 """
@@ -1020,30 +1054,31 @@ def test_get_weighted_torques_negative_moi_sets_zero(self):
10201054 """
10211055 universe_operations = UniverseOperations ()
10221056 level_manager = LevelManager (universe_operations )
1057+ axes_manager = AxesManager ()
10231058
10241059 bead = MagicMock ()
1025- # r=(1,0,0), F=(0,1,0) => raw torque in z is non-zero
10261060 bead .positions = np .array ([[1.0 , 0.0 , 0.0 ]])
10271061 bead .forces = np .array ([[0.0 , 1.0 , 0.0 ]])
1062+ bead .dimensions = np .array ([10.0 , 10.0 , 10.0 ])
10281063
10291064 rot_axes = np .identity (3 )
10301065 center = np .array ([0.0 , 0.0 , 0.0 ])
10311066 force_partitioning = 0.5
1032-
1033- # Negative MOI in z dimension
10341067 moment_of_inertia = np .array ([1.0 , 1.0 , - 1.0 ])
10351068
1036- result = level_manager .get_weighted_torques (
1037- bead = bead ,
1038- rot_axes = rot_axes ,
1039- center = center ,
1040- force_partitioning = force_partitioning ,
1041- moment_of_inertia = moment_of_inertia ,
1042- )
1069+ with patch .object (
1070+ AxesManager , "get_vector" , return_value = bead .positions - center
1071+ ):
1072+ result = level_manager .get_weighted_torques (
1073+ bead = bead ,
1074+ rot_axes = rot_axes ,
1075+ center = center ,
1076+ force_partitioning = force_partitioning ,
1077+ moment_of_inertia = moment_of_inertia ,
1078+ axes_manager = axes_manager ,
1079+ )
10431080
1044- # z torque would be non-zero, but negative MOI => z component forced to 0
1045- expected = np .array ([0.0 , 0.0 , 0.0 ])
1046- np .testing .assert_array_equal (result , expected )
1081+ np .testing .assert_array_equal (result , np .zeros (3 ))
10471082
10481083 def test_create_submatrix_basic_outer_product (self ):
10491084 """
0 commit comments