From 58bafed0cc413e4ee9d11c806c6434d2c1e634cc Mon Sep 17 00:00:00 2001 From: Kareem Farid Date: Sun, 10 Nov 2024 14:11:31 +0200 Subject: [PATCH 1/5] Squashed commit of the following: commit e7a0e71a9ea5973a0fd58fb8fa7dfc18253d26e5 Author: Kareem Farid Date: Sun Nov 3 13:22:53 2024 +0200 lint Signed-off-by: Kareem Farid commit 9e25b720f8ed1dd85ce40f1450c70bd1ea81f84f Author: Kareem Farid Date: Sun Nov 3 13:11:51 2024 +0200 update changelog, Verilator.Lint, Yosys.JsonHeader Signed-off-by: Kareem Farid commit 36bca9b006f98ecec1273884e7fb1fb25f7ba837 Author: Kareem Farid Date: Sun Nov 3 12:52:59 2024 +0200 bugfix: allow unsetting variables in configuration files Signed-off-by: Kareem Farid commit b0246e634e31fbf8c6e22ef1cc46ef4ffd1e5201 Author: Kareem Farid Date: Tue Oct 29 15:29:20 2024 +0300 hotfix: incorrect error message when substiuting with a nonexistent step commit 19109e1ee696bcbb6986499d1d8492c14ea954df Author: Mohamed Gaber Date: Sat Oct 26 20:12:12 2024 +0300 bugfix: odb steps dropping PYTHONPATH (#592) * `Odb.*` * Fixed OpenROAD dropping user-set `PYTHONPATH` values. ## Tool Updates * Use `NIX_PYTHONPATH` instead of `PYTHONPATH` in Docker and devshells to avoid collisions with user-set `PYTHONPATH` variables. commit 627a5ab58c19fe871b0256da6f2f2d824689de56 Author: Mohamed Gaber Date: Thu Oct 24 11:03:55 2024 +0300 nix: make flake.createOpenLane shell more ergonomic ## Tool Updates * `flake.createOpenLaneShell` now gets OpenLane from `python3.pkgs`. * Fixed issue with `flake.createOpenLaneShell` where plugins would not get included due to an operator precedence issue. commit 845150825c8c15975f9f6921e90f1a43e76514cb Author: Mohamed Gaber Date: Thu Oct 24 10:57:58 2024 +0300 hotfix: regression in nix/create-shell.nix commit 1bfa43d34dcab6f859bb699b182948eda91884e2 Author: Mohamed Gaber Date: Mon Oct 21 21:05:05 2024 +0300 Temporarily revert Colab's OpenLane version until it's fixed Signed-off-by: Kareem Farid From af4a258d99b7a16ad0fd42bb3d813c58ba186564 Mon Sep 17 00:00:00 2001 From: Kareem Farid Date: Fri, 29 Nov 2024 00:31:05 +0200 Subject: [PATCH 2/5] feat: convert OR detailed router drc file to klayout xml Signed-off-by: Kareem Farid --- openlane/common/drc.py | 89 +++++++++++++++++++++++++++++-- openlane/scripts/openroad/drt.tcl | 4 +- openlane/steps/openroad.py | 18 ++++++- 3 files changed, 103 insertions(+), 8 deletions(-) diff --git a/openlane/common/drc.py b/openlane/common/drc.py index 18a4d05f7..62e798dda 100644 --- a/openlane/common/drc.py +++ b/openlane/common/drc.py @@ -20,7 +20,18 @@ from dataclasses import dataclass, field, asdict from typing import List, Optional, Tuple, Dict -BoundingBox = Tuple[Decimal, Decimal, Decimal, Decimal] # microns + +@dataclass +class BoundingBox: + llx: Decimal + lly: Decimal + urx: Decimal + ury: Decimal + + +@dataclass +class BoundingBoxWithDescription(BoundingBox): + description: Optional[str] = None @dataclass @@ -54,6 +65,67 @@ class DRC: module: str violations: Dict[str, Violation] + @classmethod + def from_openroad( + Self, + report: io.TextIOWrapper, + module: str, + ) -> Tuple["DRC", int]: + class State(IntEnum): + vio_type = 0 + src = 1 + bbox = 10 + + re_violation = re.compile(r"violation type: (?P.*)$") + re_src = re.compile(r"srcs: (?P\S+)( (?P\S+))?") + re_bbox = re.compile( + r"bbox = \( (?P\S+), (?P\S+) \) - \( (?P\S+), (?P\S+) \) on Layer (?P\S+)" + ) + bbox_count = 0 + violations: Dict[str, Violation] = {} + state = State.vio_type + vio_type = src1 = src2 = lly = llx = urx = ury = "" + for line in report: + line = line.strip() + if state == State.vio_type: + vio_match = re_violation.match(line) + assert vio_match is not None, "Error while parsing drc report file" + vio_type = vio_match.group("type") + state = State.src + elif state == State.src: + src_match = re_src.match(line) + assert src_match is not None, "Error while parsing drc report file" + src1 = src_match.group("src1") + src2 = src_match.group("src2") + state = State.bbox + elif state == State.bbox: + bbox_match = re_bbox.match(line) + assert bbox_match is not None, "Error while parsing drc report file" + llx = bbox_match.group("llx") + lly = bbox_match.group("lly") + urx = bbox_match.group("urx") + ury = bbox_match.group("ury") + layer = bbox_match.group("layer") + bbox_count += 1 + bounding_box = BoundingBoxWithDescription( + Decimal(llx), + Decimal(lly), + Decimal(urx), + Decimal(ury), + f"{src1} to {src2}", + ) + violation = (layer, vio_type) + description = vio_type + if violations.get(vio_type) is not None: + violations[vio_type].bounding_boxes.append(bounding_box) + else: + violations[vio_type] = Violation( + [violation], description, [bounding_box] + ) + state = State.vio_type + + return (Self(module, violations), bbox_count) + @classmethod def from_magic( Self, @@ -125,7 +197,7 @@ class State(IntEnum): f"invalid bounding box at line {i}: bounding box has {len(coord_list)}/4 elements" ) - bounding_box: BoundingBox = ( + bounding_box = BoundingBox( coord_list[0], coord_list[1], coord_list[2], @@ -155,7 +227,7 @@ def from_magic_feedback( "Invalid syntax: 'box' command has less than 4 arguments" ) lx, ly, ux, uy = components[0:4] - last_bounding_box = ( + last_bounding_box = BoundingBox( Decimal(lx) * cif_scale, Decimal(ly) * cif_scale, Decimal(ux) * cif_scale, @@ -239,7 +311,14 @@ def to_klayout_xml(self, out: io.BufferedIOBase): multiplicity.text = str(len(violation.bounding_boxes)) xf.write(cell, category, visited, multiplicity) with xf.element("values"): - llx, lly, urx, ury = bounding_box value = ET.Element("value") - value.text = f"polygon: ({llx},{lly};{urx},{lly};{urx},{ury};{llx},{ury})" + value.text = f"polygon: ({bounding_box.llx},{bounding_box.lly};{bounding_box.urx},{bounding_box.lly};{bounding_box.urx},{bounding_box.ury};{bounding_box.llx},{bounding_box.ury})" xf.write(value) + if isinstance( + bounding_box, BoundingBoxWithDescription + ): + value = ET.Element("value") + value.text = ( + f"text: '{bounding_box.description}'" + ) + xf.write(value) diff --git a/openlane/scripts/openroad/drt.tcl b/openlane/scripts/openroad/drt.tcl index 5e8d13397..24332254b 100755 --- a/openlane/scripts/openroad/drt.tcl +++ b/openlane/scripts/openroad/drt.tcl @@ -29,9 +29,9 @@ if { [info exists ::env(DRT_MAX_LAYER)] } { detailed_route\ -bottom_routing_layer $min_layer\ -top_routing_layer $max_layer\ - -output_drc $::env(STEP_DIR)/$::env(DESIGN_NAME).drc\ + -output_drc $::env(_DRC_REPORT_PATH)\ -droute_end_iter $::env(DRT_OPT_ITERS)\ -or_seed 42\ -verbose 1 -write_views \ No newline at end of file +write_views diff --git a/openlane/steps/openroad.py b/openlane/steps/openroad.py index a358145e0..40bbbc263 100644 --- a/openlane/steps/openroad.py +++ b/openlane/steps/openroad.py @@ -72,6 +72,7 @@ from ..common import ( Path, TclUtils, + DRC as DRCObject, get_script_dir, mkdirp, aggregate_metrics, @@ -1615,9 +1616,24 @@ def get_script_path(self): def run(self, state_in: State, **kwargs) -> Tuple[ViewsUpdate, MetricsUpdate]: kwargs, env = self.extract_env(kwargs) + report_path = os.path.join(self.step_dir, "reports", "drc.rpt") + klayout_db_path = os.path.join(self.step_dir, "reports", "drc.xml") + mkdirp(os.path.join(self.step_dir, "reports")) env["DRT_THREADS"] = env.get("DRT_THREADS", str(_get_process_limit())) + env["_DRC_REPORT_PATH"] = report_path info(f"Running TritonRoute with {env['DRT_THREADS']} threads…") - return super().run(state_in, env=env, **kwargs) + views_updates, metrics_updates = super().run(state_in, env=env, **kwargs) + drc, violation_count = DRCObject.from_openroad( + open(report_path, encoding="utf8"), self.config["DESIGN_NAME"] + ) + + drc.to_klayout_xml(open(klayout_db_path, "wb")) + if violation_count > 0: + self.warn( + f"DRC errors found after routing. View the report file at {report_path}.\nView KLayout xml file at {klayout_db_path}" + ) + + return views_updates, metrics_updates @Step.factory.register() From 636ac59d0af5cbf83d8e9e874f24ecef9230eb67 Mon Sep 17 00:00:00 2001 From: Kareem Farid Date: Fri, 29 Nov 2024 00:52:10 +0200 Subject: [PATCH 3/5] test: update magic drc test cases to use BoundingBox dataclass Signed-off-by: Kareem Farid --- openlane/common/__init__.py | 2 +- test/common/test_misc_utils.py | 54 +++++++++++++++++----------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/openlane/common/__init__.py b/openlane/common/__init__.py index 1ce56762c..c34b8c9a3 100644 --- a/openlane/common/__init__.py +++ b/openlane/common/__init__.py @@ -55,7 +55,7 @@ ScopedFile, ) from .toolbox import Toolbox -from .drc import DRC, Violation +from .drc import DRC, Violation, BoundingBox from . import cli from .tpe import get_tpe, set_tpe from .ring_buffer import RingBuffer diff --git a/test/common/test_misc_utils.py b/test/common/test_misc_utils.py index 289fc82b9..9ca765768 100644 --- a/test/common/test_misc_utils.py +++ b/test/common/test_misc_utils.py @@ -92,7 +92,7 @@ def test_slugify(): def test_magic_drc(): - from openlane.common import DRC, Violation + from openlane.common import DRC, Violation, BoundingBox drc_object, count = DRC.from_magic(io.StringIO(MAGIC_EXAMPLE)) violations = { @@ -100,19 +100,19 @@ def test_magic_drc(): rules=[("LU", "3")], description="P-diff distance to N-tap must be < 15.0um (LU.3)", bounding_boxes=[ - ( + BoundingBox( Decimal("17.990"), Decimal("21.995"), Decimal("18.265"), Decimal("22.995"), ), - ( + BoundingBox( Decimal("20.905"), Decimal("22.935"), Decimal("21.575"), Decimal("22.995"), ), - ( + BoundingBox( Decimal("18.535"), Decimal("21.995"), Decimal("18.795"), @@ -171,62 +171,62 @@ def test_magic_drc_exceptions(): def test_magic_feedback(): - from openlane.common import DRC, Violation + from openlane.common import DRC, Violation, BoundingBox expected_violations = { "obsm4-metal4.ILLEGAL_OVERLAP": Violation( rules=[("obsm4-metal4", "ILLEGAL_OVERLAP")], description="Illegal overlap between obsm4 and metal4 (types do not connect)", bounding_boxes=[ - ( + BoundingBox( Decimal("11137.80"), Decimal("4449.70"), Decimal("11153.80"), Decimal("4458.80"), ), - ( + BoundingBox( Decimal("11137.80"), Decimal("4437.90"), Decimal("11139.90"), Decimal("4449.70"), ), - ( + BoundingBox( Decimal("11151.70"), Decimal("4437.90"), Decimal("11153.80"), Decimal("4449.70"), ), - ( + BoundingBox( Decimal("11137.80"), Decimal("4433.70"), Decimal("11153.80"), Decimal("4437.90"), ), - ( + BoundingBox( Decimal("11137.80"), Decimal("4421.90"), Decimal("11139.90"), Decimal("4433.70"), ), - ( + BoundingBox( Decimal("11151.70"), Decimal("4421.90"), Decimal("11153.80"), Decimal("4433.70"), ), - ( + BoundingBox( Decimal("11137.80"), Decimal("4417.70"), Decimal("11153.80"), Decimal("4421.90"), ), - ( + BoundingBox( Decimal("11137.80"), Decimal("4412.80"), Decimal("11139.90"), Decimal("4417.70"), ), - ( + BoundingBox( Decimal("11151.70"), Decimal("4412.80"), Decimal("11153.80"), @@ -238,19 +238,19 @@ def test_magic_feedback(): rules=[("obsm4-via4", "ILLEGAL_OVERLAP")], description="Illegal overlap between obsm4 and via4 (types do not connect)", bounding_boxes=[ - ( + BoundingBox( Decimal("11139.90"), Decimal("4437.90"), Decimal("11151.70"), Decimal("4449.70"), ), - ( + BoundingBox( Decimal("11139.90"), Decimal("4421.90"), Decimal("11151.70"), Decimal("4433.70"), ), - ( + BoundingBox( Decimal("11139.90"), Decimal("4412.80"), Decimal("11151.70"), @@ -262,31 +262,31 @@ def test_magic_feedback(): rules=[("UNKNOWN", "UNKNOWN2")], description="device missing 1 terminal;\n connecting remainder to node VGND", bounding_boxes=[ - ( + BoundingBox( Decimal("8232.15"), Decimal("8080.15"), Decimal("8238.05"), Decimal("8085.65"), ), - ( + BoundingBox( Decimal("8204.55"), Decimal("8080.15"), Decimal("8224.25"), Decimal("8085.65"), ), - ( + BoundingBox( Decimal("8186.15"), Decimal("8035.95"), Decimal("8215.05"), Decimal("8041.45"), ), - ( + BoundingBox( Decimal("8149.35"), Decimal("8080.15"), Decimal("8196.65"), Decimal("8085.65"), ), - ( + BoundingBox( Decimal("393.75"), Decimal("8025.75"), Decimal("404.25"), @@ -298,31 +298,31 @@ def test_magic_feedback(): rules=[("UNKNOWN", "UNKNOWN3")], description="device missing 1 terminal;\n connecting remainder to node VPWR", bounding_boxes=[ - ( + BoundingBox( Decimal("8232.15"), Decimal("8063.15"), Decimal("8238.05"), Decimal("8071.85"), ), - ( + BoundingBox( Decimal("8204.55"), Decimal("8063.15"), Decimal("8224.25"), Decimal("8071.85"), ), - ( + BoundingBox( Decimal("8186.15"), Decimal("8049.75"), Decimal("8215.05"), Decimal("8058.45"), ), - ( + BoundingBox( Decimal("8149.35"), Decimal("8063.15"), Decimal("8196.65"), Decimal("8071.85"), ), - ( + BoundingBox( Decimal("393.75"), Decimal("8008.75"), Decimal("404.25"), From 5c76e549acbc1263660d76d885e854aca6c898c0 Mon Sep 17 00:00:00 2001 From: Kareem Farid Date: Fri, 29 Nov 2024 17:43:46 +0200 Subject: [PATCH 4/5] Revert "test: update magic drc test cases to use BoundingBox dataclass" This reverts commit 636ac59d0af5cbf83d8e9e874f24ecef9230eb67. --- openlane/common/__init__.py | 2 +- test/common/test_misc_utils.py | 54 +++++++++++++++++----------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/openlane/common/__init__.py b/openlane/common/__init__.py index c34b8c9a3..1ce56762c 100644 --- a/openlane/common/__init__.py +++ b/openlane/common/__init__.py @@ -55,7 +55,7 @@ ScopedFile, ) from .toolbox import Toolbox -from .drc import DRC, Violation, BoundingBox +from .drc import DRC, Violation from . import cli from .tpe import get_tpe, set_tpe from .ring_buffer import RingBuffer diff --git a/test/common/test_misc_utils.py b/test/common/test_misc_utils.py index 9ca765768..289fc82b9 100644 --- a/test/common/test_misc_utils.py +++ b/test/common/test_misc_utils.py @@ -92,7 +92,7 @@ def test_slugify(): def test_magic_drc(): - from openlane.common import DRC, Violation, BoundingBox + from openlane.common import DRC, Violation drc_object, count = DRC.from_magic(io.StringIO(MAGIC_EXAMPLE)) violations = { @@ -100,19 +100,19 @@ def test_magic_drc(): rules=[("LU", "3")], description="P-diff distance to N-tap must be < 15.0um (LU.3)", bounding_boxes=[ - BoundingBox( + ( Decimal("17.990"), Decimal("21.995"), Decimal("18.265"), Decimal("22.995"), ), - BoundingBox( + ( Decimal("20.905"), Decimal("22.935"), Decimal("21.575"), Decimal("22.995"), ), - BoundingBox( + ( Decimal("18.535"), Decimal("21.995"), Decimal("18.795"), @@ -171,62 +171,62 @@ def test_magic_drc_exceptions(): def test_magic_feedback(): - from openlane.common import DRC, Violation, BoundingBox + from openlane.common import DRC, Violation expected_violations = { "obsm4-metal4.ILLEGAL_OVERLAP": Violation( rules=[("obsm4-metal4", "ILLEGAL_OVERLAP")], description="Illegal overlap between obsm4 and metal4 (types do not connect)", bounding_boxes=[ - BoundingBox( + ( Decimal("11137.80"), Decimal("4449.70"), Decimal("11153.80"), Decimal("4458.80"), ), - BoundingBox( + ( Decimal("11137.80"), Decimal("4437.90"), Decimal("11139.90"), Decimal("4449.70"), ), - BoundingBox( + ( Decimal("11151.70"), Decimal("4437.90"), Decimal("11153.80"), Decimal("4449.70"), ), - BoundingBox( + ( Decimal("11137.80"), Decimal("4433.70"), Decimal("11153.80"), Decimal("4437.90"), ), - BoundingBox( + ( Decimal("11137.80"), Decimal("4421.90"), Decimal("11139.90"), Decimal("4433.70"), ), - BoundingBox( + ( Decimal("11151.70"), Decimal("4421.90"), Decimal("11153.80"), Decimal("4433.70"), ), - BoundingBox( + ( Decimal("11137.80"), Decimal("4417.70"), Decimal("11153.80"), Decimal("4421.90"), ), - BoundingBox( + ( Decimal("11137.80"), Decimal("4412.80"), Decimal("11139.90"), Decimal("4417.70"), ), - BoundingBox( + ( Decimal("11151.70"), Decimal("4412.80"), Decimal("11153.80"), @@ -238,19 +238,19 @@ def test_magic_feedback(): rules=[("obsm4-via4", "ILLEGAL_OVERLAP")], description="Illegal overlap between obsm4 and via4 (types do not connect)", bounding_boxes=[ - BoundingBox( + ( Decimal("11139.90"), Decimal("4437.90"), Decimal("11151.70"), Decimal("4449.70"), ), - BoundingBox( + ( Decimal("11139.90"), Decimal("4421.90"), Decimal("11151.70"), Decimal("4433.70"), ), - BoundingBox( + ( Decimal("11139.90"), Decimal("4412.80"), Decimal("11151.70"), @@ -262,31 +262,31 @@ def test_magic_feedback(): rules=[("UNKNOWN", "UNKNOWN2")], description="device missing 1 terminal;\n connecting remainder to node VGND", bounding_boxes=[ - BoundingBox( + ( Decimal("8232.15"), Decimal("8080.15"), Decimal("8238.05"), Decimal("8085.65"), ), - BoundingBox( + ( Decimal("8204.55"), Decimal("8080.15"), Decimal("8224.25"), Decimal("8085.65"), ), - BoundingBox( + ( Decimal("8186.15"), Decimal("8035.95"), Decimal("8215.05"), Decimal("8041.45"), ), - BoundingBox( + ( Decimal("8149.35"), Decimal("8080.15"), Decimal("8196.65"), Decimal("8085.65"), ), - BoundingBox( + ( Decimal("393.75"), Decimal("8025.75"), Decimal("404.25"), @@ -298,31 +298,31 @@ def test_magic_feedback(): rules=[("UNKNOWN", "UNKNOWN3")], description="device missing 1 terminal;\n connecting remainder to node VPWR", bounding_boxes=[ - BoundingBox( + ( Decimal("8232.15"), Decimal("8063.15"), Decimal("8238.05"), Decimal("8071.85"), ), - BoundingBox( + ( Decimal("8204.55"), Decimal("8063.15"), Decimal("8224.25"), Decimal("8071.85"), ), - BoundingBox( + ( Decimal("8186.15"), Decimal("8049.75"), Decimal("8215.05"), Decimal("8058.45"), ), - BoundingBox( + ( Decimal("8149.35"), Decimal("8063.15"), Decimal("8196.65"), Decimal("8071.85"), ), - BoundingBox( + ( Decimal("393.75"), Decimal("8008.75"), Decimal("404.25"), From d14c1aa65e058be6215a0e4cf4a17cfe62c3c4e5 Mon Sep 17 00:00:00 2001 From: Kareem Farid Date: Fri, 29 Nov 2024 17:47:08 +0200 Subject: [PATCH 5/5] dont break API for BoundingBox Signed-off-by: Kareem Farid --- openlane/common/drc.py | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/openlane/common/drc.py b/openlane/common/drc.py index 62e798dda..7f39076bc 100644 --- a/openlane/common/drc.py +++ b/openlane/common/drc.py @@ -18,27 +18,20 @@ from enum import IntEnum from decimal import Decimal, InvalidOperation from dataclasses import dataclass, field, asdict -from typing import List, Optional, Tuple, Dict +from typing import List, Optional, Tuple, Dict, Union -@dataclass -class BoundingBox: - llx: Decimal - lly: Decimal - urx: Decimal - ury: Decimal - - -@dataclass -class BoundingBoxWithDescription(BoundingBox): - description: Optional[str] = None +BoundingBox = Tuple[Decimal, Decimal, Decimal, Decimal] # microns +BoundingBoxWithDescription = Tuple[Decimal, Decimal, Decimal, Decimal, str] # microns @dataclass class Violation: rules: List[Tuple[str, str]] # (layer, rule) description: str - bounding_boxes: List[BoundingBox] = field(default_factory=list) + bounding_boxes: List[Union[BoundingBox, BoundingBoxWithDescription]] = field( + default_factory=list + ) @property def layer(self) -> str: @@ -107,7 +100,7 @@ class State(IntEnum): ury = bbox_match.group("ury") layer = bbox_match.group("layer") bbox_count += 1 - bounding_box = BoundingBoxWithDescription( + bounding_box = ( Decimal(llx), Decimal(lly), Decimal(urx), @@ -197,7 +190,7 @@ class State(IntEnum): f"invalid bounding box at line {i}: bounding box has {len(coord_list)}/4 elements" ) - bounding_box = BoundingBox( + bounding_box = ( coord_list[0], coord_list[1], coord_list[2], @@ -227,7 +220,7 @@ def from_magic_feedback( "Invalid syntax: 'box' command has less than 4 arguments" ) lx, ly, ux, uy = components[0:4] - last_bounding_box = BoundingBox( + last_bounding_box = ( Decimal(lx) * cif_scale, Decimal(ly) * cif_scale, Decimal(ux) * cif_scale, @@ -312,13 +305,9 @@ def to_klayout_xml(self, out: io.BufferedIOBase): xf.write(cell, category, visited, multiplicity) with xf.element("values"): value = ET.Element("value") - value.text = f"polygon: ({bounding_box.llx},{bounding_box.lly};{bounding_box.urx},{bounding_box.lly};{bounding_box.urx},{bounding_box.ury};{bounding_box.llx},{bounding_box.ury})" + value.text = f"polygon: ({bounding_box[0]},{bounding_box[1]};{bounding_box[2]},{bounding_box[1]};{bounding_box[2]},{bounding_box[3]};{bounding_box[0]},{bounding_box[3]})" xf.write(value) - if isinstance( - bounding_box, BoundingBoxWithDescription - ): + if len(bounding_box) == 5: value = ET.Element("value") - value.text = ( - f"text: '{bounding_box.description}'" - ) + value.text = f"text: '{bounding_box[4]}'" xf.write(value)