Repository: tqec/tqec

Issue:
[Question]: Is there a method to change labels?
**Question**: Is there a method to change labels? 

I looked for it, but I don't find it.

I need to relabel to use `tqec.computation.BlockGraph.compose` whit two imported .dae files. The issue is that ports are automatically labeled, so if you import two .dae files to merge them, you must change the labels of one of the BlockGraphs. If you don't relabel it raises an exception because there are ports with the same label, and they cannot merge into a new one. 

```
    block_graph = BlockGraph.from_dae_file("slide67_open.dae")
    block_graph2 = BlockGraph.from_dae_file("cnot_4_test.dae")
    block_graph.view_as_html("composeBG1.html")
    block_graph2.view_as_html("composeBG2.html")
    
    print(f"Cubes BG1:")
    for cube in block_graph.cubes:
        print(cube, cube.label)
    print(f"Cubes BG2:")

    block_graph3 = block_graph.compose(block_graph2, "Port4", "Port2_0")
    block_graph3.view_as_html("composeBGfinal.html")

```
If you don't relabel, it fails. If relabel, works perfect. @inmzhang if it doesn't already exist, I would like to code it.

Related: **could we create a method to cap open ports by label?** I'd also like to code it. Maybe now it look unnecessary, but if we have later a frontend for users, could be nice they don't have to create new models in SketchUp to cap ports.

Hints from issue comments:
@nelimee: I do not think there is any method to rename labels. A function to do that will likely look like the existing 

https://github.com/tqec/tqec/blob/c0a5a34f554446bd202d45af92d0929294fcb482/src/tqec/computation/block_graph.py#L495-L524

Same remark for the capping by label, we do not have that, but it can be done using the above method quite easily already.@angelaelisa: Thanks Adrien, you're right I forgot about fill_ports@angelaelisa: @nelimee I've been testing and I think there is a bug: `fill_ports` allows me to use any string as kind of cube:

```
    print(f"Cubes BG2:")
    for cube in block_graph2.cubes:
        print(cube, cube.label)

    block_graph2.fill_ports({"Port0": "R"})

    print(f"New cubes BG2:")
    for cube in block_graph2.cubes:
        print(cube, cube.label)
```

It prints (look at **R(0,0,0)**):

```
Cubes BG2:
ZXX(0,0,1) 
ZXX(0,0,2) 
ZXX(0,1,1) 
ZXZ(0,1,2) 
ZXZ(1,1,2) 
ZXZ(1,1,1) 
PORT(0,0,0) Port0
PORT(0,0,3) Port1
PORT(1,1,3) Port2
PORT(1,1,0) Port3
New cubes BG2:
ZXX(0,0,1) 
ZXX(0,0,2) 
ZXX(0,1,1) 
ZXZ(0,1,2) 
ZXZ(1,1,2) 
ZXZ(1,1,1) 
R(0,0,0)
PORT(0,0,3) Port1
PORT(1,1,3) Port2
PORT(1,1,0) Port3
```
Another problem is that it's design to convert into ZXCubes without labels, so at the end of the function it pops the label. I think we should:
- Raise an exception when the arg contains a string that is not PORT or ZXCUBE
- Convert the string "P" into "PORT" (there is no conversion, so it creates the cube with the string you introduce"
- Allow to give a new label to the cube created@nelimee: > [@nelimee](https://github.com/nelimee) I've been testing and I think there is a bug: `fill_ports` allows me to use any string as kind of cube:

The typing information explicitly say that the provided `fill` argument should be either `CubeKind` or `Mapping[str, CubeKind]`. It is expected that users respect the typing information. I am on the side of not over-checking the inputs, I think I remember that Python's philosophy goes this way too. 

Nevertheless, I agree that's quite confusing, I would expect things to break, maybe an additional check would be nice? 

And yes, for renaming ports, a new method will have to be created, but can likely be inspired by `fill_ports`.@angelaelisa: > Nevertheless, I agree that's quite confusing, I would expect things to break, maybe an additional check would be nice?

Maybe we can add a check and use `def cube_kind_from_string(s: str) -> CubeKind:` to make its behavior more similar to `add_cube`. 


> Python's philosophy goes this way too

Sorry, I'm used to building apps whose users are lawyers... that's why I'm a control freak :)


> And yes, for renaming ports, a new method will have to be created, but can likely be inspired by `fill_ports`.

If you don't mind, I'll work on it this weekend to submit a PR for revision @inmzhang: > Maybe we can add a check and use `def cube_kind_from_string(s: str) -> CubeKind:` to make its behavior more similar to `add_cube`.

That is a good idea.

> If you don't mind, I'll work on it this weekend to submit a PR for revision

A method like `relabel_cube` that can be used for both cubes and ports seems good to me.@angelaelisa: I'm going to start working on `relabel_cube`, because I think it would be fine to use it in `fill_ports` to allow to relabel the new filled cube. 

I think those should be the rules in `relabel_cube`:

1. If the cube kind is a port, its label must be unique.
2. If the cube kind is not a port, its label can be repeated, but never the label of a port.
3. Raise an exception if the new label violates these rules.

I think it should:

1. Check if the cube exists: It would be nice to allow to identify the cube by position and by label (by label would allow to change multiple ZXCubes at once)
2. Check the str respect the rules for labeling
3. Create a new cube instance because of `@dataclass(frozen=True)` 
4. Update the graph (I still must study how far should be updated)

I write all this for a double purpose: describing the problem and the path to remove it I think is itself the 50% of the solution, and makes it easier to be supervised.@angelaelisa: Hy!

I really think we should use the `add_cube()` method to fill the ports instead of placing the cubes directly. Right now, there's no validation happening, so users can fill ports with any cube kind, even if it doesn't match or fit the edge direction.

![Image](https://github.com/user-attachments/assets/e19e5f78-1d0f-4b7b-acc3-11cee0962a3d)

This makes it super hard to manage on the frontend, because we’d need to replicate all those edge checks in JavaScript. I feel like it'd be way cleaner and more robust to let the backend handle that logic properly. What do you think?@inmzhang: Hi Angela,

To be clear, `add_cube` does not check the color matching rule. You need to call `BlockGraph.validate` to check for the rules. If you'd like to have an automatic way to fill the ports, you can call `BlockGraph.fill_ports_for_minimal_simulation()`, which will return a list of `FilledGraph`.@angelaelisa: Many thanks for clarifying, @inmzhang! 🙏

I’ve tried to implement `BlockGraph.fill_ports_for_minimal_simulation()`, but I wasn’t able to get it working yet. I’ll try to isolate it in a simpler file to better understand why it’s not running properly in the frontend. I hope I’ll figure it out soon 🤞@inmzhang: > I’ve tried to implement `BlockGraph.fill_ports_for_minimal_simulation()`, but I wasn’t able to get it working yet. I’ll try to isolate it in a simpler file to better understand why it’s not running properly in the frontend. I hope I’ll figure it out soon 🤞

Please feel free to reach out if you have problems with that, there is the possibility that the function itself has bugs hidden somewhere.@angelaelisa: Hi @inmzhang

I’ve pushed the Flask app to a personal repo for now:
https://github.com/angelaelisa/flaskapp

I didn't have time to migrate it to React yet. I’ll keep working on polishing it a bit more until I feel it's solid enough to move it to `tqec/flaskapp`. Thanks again@angelaelisa: > Please feel free to reach out if you have problems with that, there is the possibility that the function itself has bugs hidden somewhere.

@inmzhang It's working perfect! The problem was that I was trying to use `fg = filled_graphs[0].block_graph` instead of `fg = filled_graphs[0].graph`. It was my fault :) 

```
from tqec import BlockGraph
from tqec.interop.collada.html_viewer import display_collada_model
from pathlib import Path

INPUT_FILE = Path("cnot_4_test.dae")
OUTPUT_FILE = Path("minimal_test.html")

print(f"Loading: {INPUT_FILE}")
bg = BlockGraph.from_dae_file(INPUT_FILE)

print("Trying to fill ports minimally...")
filled_graphs = bg.fill_ports_for_minimal_simulation()

if not filled_graphs:
    raise ValueError("No filled graphs returned.")

fg = filled_graphs[0].graph

print("Minimal fill completed.")

viewer = fg.view_as_html(OUTPUT_FILE)

```
It works perfect. Many thanks!!

Solution patch:
diff --git a/src/tqec/computation/block_graph.py b/src/tqec/computation/block_graph.py
index 735d45ed..855d5060 100644
--- a/src/tqec/computation/block_graph.py
+++ b/src/tqec/computation/block_graph.py
@@ -894,6 +894,65 @@ class BlockGraph:
             obj_dict = json.loads(json_text)
         return BlockGraph.from_dict(obj_dict)
 
+    def relabel_cubes(self, label_mapping: Mapping[Position3D | str, str]) -> None:
+        """Relabel cubes in the block graph.
+
+        This method updates the labels of cubes in the graph, based on a mapping
+        from either a cube position or its existing label to a new label.
+
+        Args:
+            label_mapping: A mapping from either Position3D or current cube label (str)
+                to the new label to assign.
+
+        Raises:
+            TQECException: If a cube is not found for the given key, if a port label
+                is reused, or if the new label conflicts with existing port labels.
+        """
+        port_labels = {cube.label for cube in self.cubes if cube.is_port}
+        assigned_new_labels: set[str] = set()
+
+        for key, new_label in label_mapping.items():
+            if not new_label:
+                raise TQECException("New label must be non-empty.")
+
+            if isinstance(key, Position3D):
+                matching_cubes = [cube for cube in self.cubes if cube.position == key]
+            elif isinstance(key, str):
+                matching_cubes = [cube for cube in self.cubes if cube.label == key]
+            else:
+                raise TQECException(
+                    f"Invalid identifier '{key}'. Must be Position3D or str."
+                )
+
+            if not matching_cubes:
+                raise TQECException(f"No cube found for identifier '{key}'.")
+
+            is_port_relabel = any(cube.is_port for cube in matching_cubes)
+
+            if is_port_relabel:
+                if new_label in port_labels:
+                    raise TQECException(
+                        f"Port label '{new_label}' is already assigned to another port."
+                    )
+                if new_label in assigned_new_labels:
+                    raise TQECException(
+                        f"Port label '{new_label}' is reused multiple times."
+                    )
+                assigned_new_labels.add(new_label)
+            else:
+                if new_label in port_labels:
+                    raise TQECException(
+                        f"The label '{new_label}' belongs to a port and cannot be reused by a non-port cube."
+                    )
+
+            for cube in matching_cubes:
+                updated_cube = Cube(
+                    position=cube.position, kind=cube.kind, label=new_label
+                )
+                self._graph.add_node(
+                    cube.position, **{self._NODE_DATA_KEY: updated_cube}
+                )
+
 
 def block_kind_from_str(string: str) -> BlockKind:
     """Parse a block kind from a string."""


Test patch:
diff --git a/src/tqec/computation/block_graph_test.py b/src/tqec/computation/block_graph_test.py
index e3bd0480..5f7046b9 100644
--- a/src/tqec/computation/block_graph_test.py
+++ b/src/tqec/computation/block_graph_test.py
@@ -286,3 +286,27 @@ def test_block_graph_to_from_json() -> None:
         read_g = BlockGraph.from_json(temp_file.name)
         assert read_g == g
     os.remove(temp_file.name)
+
+
+def test_block_graph_relabel_cubes() -> None:
+    g = BlockGraph()
+    n = g.add_cube(Position3D(0, 0, 0), "P", "In")
+    n2 = g.add_cube(Position3D(1, 0, 0), "ZXZ")
+    g.add_pipe(n, n2, "OXZH")
+    n3 = g.add_cube(Position3D(2, 0, 0), "P", "Out")
+    g.add_pipe(n2, n3, "OXZH")
+
+    label_mapping: dict[Position3D | str, str] = {
+        Position3D(0, 0, 0): "InputPortByPos",
+        "Out": "OutputPortByLabel",
+    }
+
+    g.relabel_cubes(label_mapping)
+
+    new_labels = {cube.label for cube in g.cubes}
+    assert "InputPortByPos" in new_labels
+    assert "OutputPortByLabel" in new_labels
+    assert "In" not in new_labels
+    assert "Out" not in new_labels
+    assert g[Position3D(0, 0, 0)].is_port
+    assert g[Position3D(2, 0, 0)].is_port