Add profile for 4x4 MPO8 shuffle cable

This commit is contained in:
Jeremy Stretch
2025-11-14 16:33:35 -05:00
parent fd8ed7a6fe
commit 654b86a0b1
4 changed files with 155 additions and 10 deletions

View File

@@ -44,7 +44,12 @@ class BaseCableProfile:
)
})
def get_mapped_position(self, position):
def get_mapped_position(self, side, position):
"""
Return the mapped position for a given cable end and position.
By default, assume all positions are symmetrical.
"""
return position
def get_peer_terminations(self, terminations, position_stack):
@@ -65,7 +70,7 @@ class BaseCableProfile:
cable_end=terminations[0].opposite_cable_end
)
if position is not None:
qs = qs.filter(position=self.get_mapped_position(position))
qs = qs.filter(position=self.get_mapped_position(local_end, position))
return qs
@@ -93,11 +98,11 @@ class BToManyCableProfile(BaseCableProfile):
b_side_numbered = False
class Shuffle2x2MPOCableProfile(BaseCableProfile):
class Shuffle2x2MPO8CableProfile(BaseCableProfile):
a_max_connections = 8
b_max_connections = 8
def get_mapped_position(self, position):
def get_mapped_position(self, side, position):
return {
1: 1,
2: 2,
@@ -108,3 +113,26 @@ class Shuffle2x2MPOCableProfile(BaseCableProfile):
7: 7,
8: 8,
}.get(position)
class Shuffle4x4MPO8CableProfile(BaseCableProfile):
a_max_connections = 8
b_max_connections = 8
# A side to B side position mapping
_a_mapping = {
1: 1,
2: 3,
3: 5,
4: 7,
5: 2,
6: 4,
7: 6,
8: 8,
}
# B side to A side position mapping
_b_mapping = {v: k for k, v in _a_mapping.items()}
def get_mapped_position(self, side, position):
if side.lower() == 'b':
return self._b_mapping.get(position)
return self._a_mapping.get(position)

View File

@@ -1722,7 +1722,8 @@ class CableProfileChoices(ChoiceSet):
STRAIGHT_MULTI = 'straight-multi'
A_TO_MANY = 'a-to-many'
B_TO_MANY = 'b-to-many'
SHUFFLE_2X2_MPO = 'shuffle-2x2-mpo'
SHUFFLE_2X2_MPO8 = 'shuffle-2x2-mpo8'
SHUFFLE_4X4_MPO8 = 'shuffle-4x4-mpo8'
CHOICES = (
(STRAIGHT_SINGLE, _('Straight (single position)')),
@@ -1730,7 +1731,8 @@ class CableProfileChoices(ChoiceSet):
# TODO: Better names for many-to-one profiles?
(A_TO_MANY, _('A to many')),
(B_TO_MANY, _('B to many')),
(SHUFFLE_2X2_MPO, _('Shuffle (2x2 MPO)')),
(SHUFFLE_2X2_MPO8, _('Shuffle (2x2 MPO8)')),
(SHUFFLE_4X4_MPO8, _('Shuffle (4x4 MPO8)')),
)

View File

@@ -138,7 +138,8 @@ class Cable(PrimaryModel):
CableProfileChoices.STRAIGHT_MULTI: cable_profiles.StraightMultiCableProfile,
CableProfileChoices.A_TO_MANY: cable_profiles.AToManyCableProfile,
CableProfileChoices.B_TO_MANY: cable_profiles.BToManyCableProfile,
CableProfileChoices.SHUFFLE_2X2_MPO: cable_profiles.Shuffle2x2MPOCableProfile,
CableProfileChoices.SHUFFLE_2X2_MPO8: cable_profiles.Shuffle2x2MPO8CableProfile,
CableProfileChoices.SHUFFLE_4X4_MPO8: cable_profiles.Shuffle4x4MPO8CableProfile,
}.get(self.profile)
def _get_x_terminations(self, side):

View File

@@ -262,7 +262,7 @@ class CablePathTests(CablePathTestCase):
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_105_cable_profile_2x2_mpo(self):
def test_105_cable_profile_2x2_mpo8(self):
"""
[IF1:1] --C1-- [IF3:1]
[IF1:2] [IF3:2]
@@ -273,9 +273,10 @@ class CablePathTests(CablePathTestCase):
[IF2:3] [IF4:3]
[IF2:4] [IF4:4]
Cable profile: Shuffle (2x2 MPO)
Cable profile: Shuffle (2x2 MPO8)
"""
interfaces = [
# A side
Interface.objects.create(device=self.device, name='Interface 1:1'),
Interface.objects.create(device=self.device, name='Interface 1:2'),
Interface.objects.create(device=self.device, name='Interface 1:3'),
@@ -284,6 +285,7 @@ class CablePathTests(CablePathTestCase):
Interface.objects.create(device=self.device, name='Interface 2:2'),
Interface.objects.create(device=self.device, name='Interface 2:3'),
Interface.objects.create(device=self.device, name='Interface 2:4'),
# B side
Interface.objects.create(device=self.device, name='Interface 3:1'),
Interface.objects.create(device=self.device, name='Interface 3:2'),
Interface.objects.create(device=self.device, name='Interface 3:3'),
@@ -296,7 +298,7 @@ class CablePathTests(CablePathTestCase):
# Create cable 1
cable1 = Cable(
profile=CableProfileChoices.SHUFFLE_2X2_MPO,
profile=CableProfileChoices.SHUFFLE_2X2_MPO8,
a_terminations=interfaces[0:8],
b_terminations=interfaces[8:16],
)
@@ -372,6 +374,118 @@ class CablePathTests(CablePathTestCase):
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_106_cable_profile_4x4_mpo8(self):
"""
[IF1:1] --C1-- [IF3:1]
[IF1:2] [IF3:2]
[IF1:3] [IF3:3]
[IF1:4] [IF3:4]
[IF2:1] [IF4:1]
[IF2:2] [IF4:2]
[IF2:3] [IF4:3]
[IF2:4] [IF4:4]
Cable profile: Shuffle (4x4 MPO8)
"""
interfaces = [
# A side
Interface.objects.create(device=self.device, name='Interface 1:1'),
Interface.objects.create(device=self.device, name='Interface 1:2'),
Interface.objects.create(device=self.device, name='Interface 2:1'),
Interface.objects.create(device=self.device, name='Interface 2:2'),
Interface.objects.create(device=self.device, name='Interface 3:1'),
Interface.objects.create(device=self.device, name='Interface 3:2'),
Interface.objects.create(device=self.device, name='Interface 4:1'),
Interface.objects.create(device=self.device, name='Interface 4:2'),
# B side
Interface.objects.create(device=self.device, name='Interface 5:1'),
Interface.objects.create(device=self.device, name='Interface 5:2'),
Interface.objects.create(device=self.device, name='Interface 6:1'),
Interface.objects.create(device=self.device, name='Interface 6:2'),
Interface.objects.create(device=self.device, name='Interface 7:1'),
Interface.objects.create(device=self.device, name='Interface 7:2'),
Interface.objects.create(device=self.device, name='Interface 8:1'),
Interface.objects.create(device=self.device, name='Interface 8:2'),
]
# Create cable 1
cable1 = Cable(
profile=CableProfileChoices.SHUFFLE_4X4_MPO8,
a_terminations=interfaces[0:8],
b_terminations=interfaces[8:16],
)
cable1.clean()
cable1.save()
paths = [
# A-to-B paths
self.assertPathExists(
(interfaces[0], cable1, interfaces[8]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[1], cable1, interfaces[10]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[2], cable1, interfaces[12]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[3], cable1, interfaces[14]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[4], cable1, interfaces[9]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[5], cable1, interfaces[11]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[6], cable1, interfaces[13]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[7], cable1, interfaces[15]), is_complete=True, is_active=True
),
# B-to-A paths
self.assertPathExists(
(interfaces[8], cable1, interfaces[0]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[9], cable1, interfaces[4]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[10], cable1, interfaces[1]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[11], cable1, interfaces[5]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[12], cable1, interfaces[2]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[13], cable1, interfaces[6]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[14], cable1, interfaces[3]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[15], cable1, interfaces[7]), is_complete=True, is_active=True
),
]
self.assertEqual(CablePath.objects.count(), len(paths))
for i, (interface, path) in enumerate(zip(interfaces, paths)):
interface.refresh_from_db()
self.assertPathIsSet(interface, path)
self.assertEqual(interface.cable_end, 'A' if i < 8 else 'B')
self.assertEqual(interface.cable_position, (i % 8) + 1)
# Test SVG generation
CableTraceSVG(interfaces[0]).render()
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_202_single_path_via_pass_through_with_breakouts(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [IF3]