Skip to content

Conversation

@ryan-kersten
Copy link
Collaborator

Closes #305

The main computational complexity in implementing this code was finding permutation R and C where
RM=MC^t for a matrix M, where R and C have an orbit of length L. I've never used GAP before, is there any better way to find these permutations than using 'filtered' to find all the symmetry group elements of a certain order?

@ryan-kersten ryan-kersten requested a review from perlinm as a code owner August 4, 2025 19:18
@ryan-kersten ryan-kersten force-pushed the balenced-product branch 2 times, most recently from a3e2d5b to e092a47 Compare August 4, 2025 21:19
@ryan-kersten
Copy link
Collaborator Author

Sorry about the somewhat messy git history- I had a bunch of type check issues and mocking issues that were a little ugly. I typically develop things in c++, contributing in python is new to me and I still have a lot to learn :)

Should be in a good state now

@perlinm
Copy link
Collaborator

perlinm commented Aug 5, 2025

No worries, the effort to pass all checks is greatly appreciated!

@perlinm
Copy link
Collaborator

perlinm commented Aug 6, 2025

Hmm, maybe if it takes a long time to find suitable R and C, and moreover the choice of these matrices is not unique, we should make them inputs to the code (and test validity/compatibility at initialization time). We can then provide tools for finding suitable choices of R and C, and provide example code in the docstring for BalancedProductCode.

Also, maybe it would be nice to add an alternative construction based on Eq 4 on page 7 of arXiv:2505.13679? Something like a BalancedProductCode.from_codes(code_q: codes.CSSCode, code_c: codes.ClassicalCode).

@ryan-kersten
Copy link
Collaborator Author

Earlier, I was thinking of caching r and c across successive calls, but passing them as inputs makes much more sense.

While adding the alternate construction method, I realized it most of the work needed for the distance balancing feature from Section III. So I went ahead and implemented that too, although I set it to default to false since it involves calculating the distance of the input codes.

Uses better way to get number of encoded logical ops in a code
@ryan-kersten
Copy link
Collaborator Author

I implemented the new features and made tests for them. Is there anything else I can improve?

@perlinm
Copy link
Collaborator

perlinm commented Aug 9, 2025

Sorry for the delay! I'll take another look next week 🙂

Copy link
Collaborator

@perlinm perlinm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all awesome and looking good! I have just a few requests. I may do one more pass over this PR afterwards, but also at some point I'm happy to just merge and perform some additional cleanup myself 🙂

Comment on lines +1598 to +1599
R: qldpc.abstract.GroupMember,
C: qldpc.abstract.GroupMember,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name these something like perm_r and perm_c and accept comb.Permutation | npt.NDArray[np.int_] inputs (with import sympy.combinatorics as comb)? I imagine many users will prefer to pass explicit permutation matrices. Initialization can convert these objects to matrices for validity checks as needed, e.g.

size = binaryMatrix.shape[0]
mat_r = perm_r.to_matrix(size) if isinstance(perm_r, comb.Permutation) else perm_r

Comment on lines +1612 to +1613
self._r = R
self._c = C
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not save the binaryMatrix to self while we're at it? (Also very minor: please rename to something like seed_matrix)

check_z = np.hstack(
(np.eye(binaryMatrix.shape[1], dtype=int) + self._r.to_matrix(), binaryMatrix)
)
CSSCode.__init__(self, check_x, check_z)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: CSSCode.__init__(self, checks_x, checks_z, field=2, is_subsystem_code=False).

Also, I think we can make checks_z ~ [1 + R, -I] and add a field: int | None = None argument that we pass to CSSCode.__init__ as well. The construction seems to work equally well for arbitrary finite fields.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you on having an arbitrary field as an argument. Where do you get the alternative definition of checks_z with the negative?

Copy link
Collaborator

@perlinm perlinm Aug 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's just from me working out on scratch paper where I can put a minus sign to make this a valid CSS code, Hx @ Hz.T = 0 (in words, this condition simply enforces that X-type and Z-type stabilizers commute). We should do the same with the other BPCode construction. Note that the minus sign has no effect for qubits (binary matrices), for which -M = M.

Comment on lines 1623 to 1637
"""
Allows for the creation of balanced codes from a classical code and a CSS code.

https://arxiv.org/pdf/2505.13679v1

If 'distance_balancing', function will determine if x and z errors have the same distance
and if not, will construct the code to maximize the final distance. This involve exact distance calculations
which will only work on relatively small codes

"""

@classmethod
def from_codes(
cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: bool = False
) -> qldpc.codes.BalancedProductCode:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the docstring inside the method:

Suggested change
"""
Allows for the creation of balanced codes from a classical code and a CSS code.
https://arxiv.org/pdf/2505.13679v1
If 'distance_balancing', function will determine if x and z errors have the same distance
and if not, will construct the code to maximize the final distance. This involve exact distance calculations
which will only work on relatively small codes
"""
@classmethod
def from_codes(
cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: bool = False
) -> qldpc.codes.BalancedProductCode:
@classmethod
def from_codes(
cls, code_q: CSSCode, code_c: ClassicalCode, distance_balancing: bool = False
) -> qldpc.codes.BalancedProductCode:
"""Construct a balanced product code from a quantum CSS code and a classical code.
If 'distance_balancing', this function will determine if x and z errors have the same distance
and if not, it will construct the code to maximize the final distance. This involve exact distance calculations
which will only work on relatively small codes.
See Eq. (4) of https://arxiv.org/pdf/2505.13679v1
"""



def get_permutation_symmetry_of_matrix(
symmetry_length: int, n: int, m: int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we give n and m descriptive names? Also, again the docstring should go inside the function.

Comment on lines 305 to 317
def parse_permutation_output(output: str) -> list[qldpc.abstract.GroupMember]:
perm_list = []
for perm in output.strip("[] ").split(", "):
result = []
cycles = re.findall(r"\([^()]+\)", perm)
for cycle in cycles:
cycle = cycle.strip("()")
elements = tuple(int(x) - 1 for x in cycle.split(","))
result.append(elements)
# Remove trivial permutation
if result:
perm_list.append(qldpc.abstract.GroupMember.from_sympy(Permutation(result)))
return perm_list
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this performing the same parsing as in this code? If so, can we factor it out into a common function?

Comment on lines 99 to 109
matrix = np.zeros([self.size] * 2, dtype=int)
size = dimension if dimension is not None else self.size
matrix = np.eye(size, dtype=int)
for ii in range(self.size):
matrix[ii, self.apply(ii)] = 1
j = self.apply(ii)
# Since using identity matrix not zero, have to clear the row
matrix[ii, :] = 0
matrix[ii, j] = 1

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the old construction and do something like

if dimension > len(matrix):
    matrix = scipy.linalg.block_diag(matrix, np.eye(dimension - len(matrix), dtype=int)
return matrix

And maybe dimension should be size?

@perlinm
Copy link
Collaborator

perlinm commented Aug 13, 2025

It looks like there are also conflicts that prevent automatic merging into main. I haven't taken a look at these conflicts, but I'm happy to help if they turn out to be thorny.

@perlinm
Copy link
Collaborator

perlinm commented Aug 14, 2025

Oh also: in keeping with naming conventions for quantum codes in qldpc, can you please rename the BalancedProductCode class to BPCode?

@ryan-kersten
Copy link
Collaborator Author

Thanks for the comments! I have time this weekend, so I should have everything good by Monday.

@perlinm
Copy link
Collaborator

perlinm commented Aug 29, 2025

@ryan-kersten could you please make a checklist of your main todo's for this PR? That way somebody else can take over to finish the PR in the future if it turns out that you don't have the time 🙂

@perlinm
Copy link
Collaborator

perlinm commented Sep 20, 2025

@ryan-kersten alternative request: to open the same PR but from a branch of this repository. That way I can finish it up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add balanced product codes

2 participants