gsim 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gsim-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: gsim
3
+ Version: 0.0.0
4
+ Author-email: flaport <floris.laporte@gmail.com>
5
+ Classifier: Programming Language :: Python :: 3.12
6
+ Classifier: Programming Language :: Python :: 3.13
7
+ Classifier: Operating System :: OS Independent
8
+ Requires-Python: <3.14,>=3.12
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: gdsfactory>=8.32.2
11
+ Requires-Dist: gdsfactoryplus>=1.3.7
12
+ Requires-Dist: gmsh
13
+ Requires-Dist: meshio>=5.0.0
14
+ Requires-Dist: plotly
15
+ Requires-Dist: pydantic>=2.10.6
16
+ Requires-Dist: pyvista>=0.43.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: altair>=5.5.0; extra == "dev"
19
+ Requires-Dist: build>=1.2.0; extra == "dev"
20
+ Requires-Dist: griffe>=1.5.6; extra == "dev"
21
+ Requires-Dist: ipykernel>=6.29.5; extra == "dev"
22
+ Requires-Dist: matplotlib>=3.10.0; extra == "dev"
23
+ Requires-Dist: mkautodoc>=0.2.0; extra == "dev"
24
+ Requires-Dist: mkdocs-autorefs>=1.3.0; extra == "dev"
25
+ Requires-Dist: mkdocs-material>=9.6.0; extra == "dev"
26
+ Requires-Dist: mkdocs-shadcn>=0.2; extra == "dev"
27
+ Requires-Dist: mkdocs>=1.6.1; extra == "dev"
28
+ Requires-Dist: mkdocstrings[python]>=0.27.0; extra == "dev"
29
+ Requires-Dist: mkinit>=1.1.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.15.0; extra == "dev"
31
+ Requires-Dist: nb-clean>=4.0.1; extra == "dev"
32
+ Requires-Dist: nbconvert>=7.16.6; extra == "dev"
33
+ Requires-Dist: papermill>=2.6.0; extra == "dev"
34
+ Requires-Dist: plotly>=6.0.0; extra == "dev"
35
+ Requires-Dist: pre-commit>=4.1.0; extra == "dev"
36
+ Requires-Dist: pyright>=1.1.0; extra == "dev"
37
+ Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
38
+ Requires-Dist: pytest-randomly>=3.16.0; extra == "dev"
39
+ Requires-Dist: pytest-xdist>=3.6.0; extra == "dev"
40
+ Requires-Dist: pytest>=8.3.0; extra == "dev"
41
+ Requires-Dist: ruff>=0.9.0; extra == "dev"
42
+ Requires-Dist: towncrier>=24.0.0; extra == "dev"
43
+ Requires-Dist: vega-datasets>=0.9.0; extra == "dev"
44
+ Requires-Dist: nbstripout>=0.8.1; extra == "dev"
45
+ Requires-Dist: ty>=0.0.13; extra == "dev"
46
+
47
+ # Gsim 0.0.0
48
+
49
+ > a GDSFactory Simulation Plugin
50
+
51
+ ![gsim-logo](./docs/assets/img/gsim-small.png)
52
+
53
+ ## Overview
54
+
55
+ Gsim bridges the gap between circuit layout design (using [GDSFactory](https://gdsfactory.github.io/gdsfactory/)) and electromagnetic simulation (using [Palace](https://awslabs.github.io/palace/)). It automates the conversion of IC component layouts into simulation-ready mesh files and configuration.
56
+
57
+ ## Features
58
+
59
+ - **Layer Stack Extraction**: Extract layer stacks from PDK definitions with a comprehensive material properties database
60
+ - **Port Configuration**: Convert GDSFactory ports into Palace-compatible port definitions (inplane, via, and CPW ports)
61
+ - **Mesh Generation**: Generate GMSH-compatible finite element meshes with configurable quality presets
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install gsim
67
+ ```
68
+
69
+ For development:
70
+
71
+ ```bash
72
+ git clone https://github.com/doplaydo/gsim
73
+ cd gsim
74
+ pip install -e .[dev]
75
+ ```
76
+
77
+ ## Quick Start
78
+
79
+ ```python
80
+ from gsim.palace import (
81
+ get_stack,
82
+ configure_inplane_port,
83
+ extract_ports,
84
+ generate_mesh,
85
+ MeshConfig,
86
+ )
87
+
88
+ # Get layer stack from active PDK
89
+ stack = get_stack()
90
+
91
+ # Configure ports on your component
92
+ configure_inplane_port(c.ports["o1"], layer="topmetal2", length=5.0)
93
+ configure_inplane_port(c.ports["o2"], layer="topmetal2", length=5.0)
94
+
95
+ # Extract configured ports
96
+ ports = extract_ports(c, stack)
97
+
98
+ # Generate mesh
99
+ result = generate_mesh(
100
+ component=c,
101
+ stack=stack,
102
+ ports=ports,
103
+ output_dir="./simulation",
104
+ config=MeshConfig.default(),
105
+ )
106
+ ```
107
+
108
+ ## Mesh Presets
109
+
110
+ | Preset | Refined Size | Max Size | Use Case |
111
+ | ------- | ------------ | -------- | --------------------------------- |
112
+ | Coarse | 10.0 µm | 600.0 µm | Fast iteration (~2.5 elements/λ) |
113
+ | Default | 5.0 µm | 300.0 µm | Balanced accuracy (~5 elements/λ) |
114
+ | Fine | 2.0 µm | 70.0 µm | High accuracy (~10 elements/λ) |
115
+
116
+ ## Port Types
117
+
118
+ - **Inplane ports**: Horizontal ports on single metal layer for CPW gaps
119
+ - **Via ports**: Vertical ports between two metal layers for microstrip structures
120
+ - **CPW ports**: Multi-element ports for proper Coplanar Waveguide excitation
121
+
122
+ ## Documentation
123
+
124
+ See the [documentation](https://doplaydo.github.io/gsim/) for detailed API reference and examples.
125
+
126
+ ## License
127
+
128
+ Copyright 2026 GDSFactory
gsim-0.0.0/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Gsim 0.0.0
2
+
3
+ > a GDSFactory Simulation Plugin
4
+
5
+ ![gsim-logo](./docs/assets/img/gsim-small.png)
6
+
7
+ ## Overview
8
+
9
+ Gsim bridges the gap between circuit layout design (using [GDSFactory](https://gdsfactory.github.io/gdsfactory/)) and electromagnetic simulation (using [Palace](https://awslabs.github.io/palace/)). It automates the conversion of IC component layouts into simulation-ready mesh files and configuration.
10
+
11
+ ## Features
12
+
13
+ - **Layer Stack Extraction**: Extract layer stacks from PDK definitions with a comprehensive material properties database
14
+ - **Port Configuration**: Convert GDSFactory ports into Palace-compatible port definitions (inplane, via, and CPW ports)
15
+ - **Mesh Generation**: Generate GMSH-compatible finite element meshes with configurable quality presets
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install gsim
21
+ ```
22
+
23
+ For development:
24
+
25
+ ```bash
26
+ git clone https://github.com/doplaydo/gsim
27
+ cd gsim
28
+ pip install -e .[dev]
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```python
34
+ from gsim.palace import (
35
+ get_stack,
36
+ configure_inplane_port,
37
+ extract_ports,
38
+ generate_mesh,
39
+ MeshConfig,
40
+ )
41
+
42
+ # Get layer stack from active PDK
43
+ stack = get_stack()
44
+
45
+ # Configure ports on your component
46
+ configure_inplane_port(c.ports["o1"], layer="topmetal2", length=5.0)
47
+ configure_inplane_port(c.ports["o2"], layer="topmetal2", length=5.0)
48
+
49
+ # Extract configured ports
50
+ ports = extract_ports(c, stack)
51
+
52
+ # Generate mesh
53
+ result = generate_mesh(
54
+ component=c,
55
+ stack=stack,
56
+ ports=ports,
57
+ output_dir="./simulation",
58
+ config=MeshConfig.default(),
59
+ )
60
+ ```
61
+
62
+ ## Mesh Presets
63
+
64
+ | Preset | Refined Size | Max Size | Use Case |
65
+ | ------- | ------------ | -------- | --------------------------------- |
66
+ | Coarse | 10.0 µm | 600.0 µm | Fast iteration (~2.5 elements/λ) |
67
+ | Default | 5.0 µm | 300.0 µm | Balanced accuracy (~5 elements/λ) |
68
+ | Fine | 2.0 µm | 70.0 µm | High accuracy (~10 elements/λ) |
69
+
70
+ ## Port Types
71
+
72
+ - **Inplane ports**: Horizontal ports on single metal layer for CPW gaps
73
+ - **Via ports**: Vertical ports between two metal layers for microstrip structures
74
+ - **CPW ports**: Multi-element ports for proper Coplanar Waveguide excitation
75
+
76
+ ## Documentation
77
+
78
+ See the [documentation](https://doplaydo.github.io/gsim/) for detailed API reference and examples.
79
+
80
+ ## License
81
+
82
+ Copyright 2026 GDSFactory
@@ -0,0 +1,217 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools>=61", "uv", "build", "wheel"]
4
+
5
+ [project]
6
+ authors = [{name = "flaport", email = "floris.laporte@gmail.com"}]
7
+ classifiers = [
8
+ "Programming Language :: Python :: 3.12",
9
+ "Programming Language :: Python :: 3.13",
10
+ "Operating System :: OS Independent"
11
+ ]
12
+ dependencies = [
13
+ "gdsfactory>=8.32.2",
14
+ "gdsfactoryplus>=1.3.7",
15
+ "gmsh",
16
+ "meshio>=5.0.0",
17
+ "plotly",
18
+ "pydantic>=2.10.6",
19
+ "pyvista>=0.43.0"
20
+ ]
21
+ description = ""
22
+ name = "gsim"
23
+ readme = "README.md"
24
+ requires-python = ">=3.12,<3.14"
25
+ version = "0.0.0"
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "altair>=5.5.0",
30
+ "build>=1.2.0",
31
+ "griffe>=1.5.6",
32
+ "ipykernel>=6.29.5",
33
+ "matplotlib>=3.10.0",
34
+ "mkautodoc>=0.2.0",
35
+ "mkdocs-autorefs>=1.3.0",
36
+ "mkdocs-material>=9.6.0",
37
+ "mkdocs-shadcn>=0.2",
38
+ "mkdocs>=1.6.1",
39
+ "mkdocstrings[python]>=0.27.0",
40
+ "mkinit>=1.1.0",
41
+ "mypy>=1.15.0",
42
+ "nb-clean>=4.0.1",
43
+ "nbconvert>=7.16.6",
44
+ "papermill>=2.6.0",
45
+ "plotly>=6.0.0",
46
+ "pre-commit>=4.1.0",
47
+ "pyright>=1.1.0",
48
+ "pytest-cov>=6.0.0",
49
+ "pytest-randomly>=3.16.0",
50
+ "pytest-xdist>=3.6.0",
51
+ "pytest>=8.3.0",
52
+ "ruff>=0.9.0",
53
+ "towncrier>=24.0.0",
54
+ "vega-datasets>=0.9.0",
55
+ "nbstripout>=0.8.1",
56
+ "ty>=0.0.13"
57
+ ]
58
+
59
+ [[tool.bver.file]]
60
+ src = "README.md"
61
+
62
+ [[tool.bver.file]]
63
+ src = "pyproject.toml"
64
+
65
+ [[tool.bver.file]]
66
+ src = "src/gsim/__init__.py"
67
+
68
+ [tool.mypy]
69
+ mypy_path = ["src"]
70
+ python_version = "3.12"
71
+ strict = true
72
+
73
+ [[tool.mypy.overrides]]
74
+ implicit_reexport = true
75
+ module = "gsim"
76
+
77
+ [[tool.mypy.overrides]]
78
+ check_untyped_defs = false
79
+ disable_error_code = ["no-untyped-def", "no-untyped-call", "var-annotated", "assignment"]
80
+ disallow_any_generics = false
81
+ disallow_untyped_defs = false
82
+ # The Palace subpackage is still evolving and uses optional dependencies
83
+ # (e.g., gmsh, plotly). Keep mypy strict overall, but relax it here so
84
+ # contributors aren't blocked by large refactors during early development.
85
+ module = "gsim.palace.*"
86
+ warn_return_any = false
87
+
88
+ [[tool.mypy.overrides]]
89
+ ignore_errors = true
90
+ module = "tests.*"
91
+
92
+ [[tool.mypy.overrides]]
93
+ ignore_missing_imports = true
94
+ module = "gmsh"
95
+
96
+ [[tool.mypy.overrides]]
97
+ ignore_missing_imports = true
98
+ module = "gmsh.*"
99
+
100
+ [[tool.mypy.overrides]]
101
+ ignore_missing_imports = true
102
+ module = "plotly"
103
+
104
+ [[tool.mypy.overrides]]
105
+ ignore_missing_imports = true
106
+ module = "plotly.*"
107
+
108
+ [tool.pylsp-mypy]
109
+ enabled = true
110
+ live_mode = true
111
+ strict = true
112
+
113
+ [tool.pyright]
114
+ exclude = ["tests", "**/__pycache__", "**/.venv"]
115
+ include = ["src"]
116
+ pythonVersion = "3.12"
117
+ reportMissingImports = "none"
118
+ reportMissingTypeStubs = "none"
119
+
120
+ [tool.pytest.ini_options]
121
+ addopts = '--tb=short'
122
+ norecursedirs = ["scripts"]
123
+ testpaths = ["tests"]
124
+
125
+ [tool.ruff]
126
+ fix = true
127
+ target-version = "py312"
128
+
129
+ [tool.ruff.format]
130
+ docstring-code-format = true
131
+
132
+ [tool.ruff.lint] # see https://docs.astral.sh/ruff/rules
133
+ ignore = [
134
+ "ANN", # flake8-annotations (too noisy for evolving API)
135
+ "BLE001", # blind exception (common around optional deps / wrappers)
136
+ "C408", # unnecessary-collection-call
137
+ "C901", # complex-structure
138
+ "COM812", # missing-trailing-comma
139
+ "D417", # undocumented-param (google convention is strict here)
140
+ "D105", # undocumented-magic-method
141
+ "EM101", # exception-must-not-use-string-literal
142
+ "EM102", # exception-must-not-use-f-string-literal
143
+ "E741", # ambiguous-variable-name
144
+ "ERA001", # commented-out-code
145
+ "FBT", # boolean-trap (fine for public API, use keywords in docs/examples)
146
+ "FIX001", # line-contains-fixme
147
+ "FIX002", # line-contains-todo
148
+ "FIX004", # line-contains-hack
149
+ "N803", # invalid-argument-name
150
+ "N806", # non-lowercase-variable-in-function
151
+ "PLC0414", # useless-import-alias
152
+ "PLR", # pylint refactor (too strict for this codebase right now)
153
+ "PLR0913", # too-many-arguments
154
+ "PLR2004", # magic-value-comparison
155
+ "PLW2901", # redefined-loop-name
156
+ "PTH123", # Path.open preferred
157
+ "S110", # try-except-pass
158
+ "RET504", # unnecessary-assign
159
+ "S324", # hashlib-insecure-hash-function
160
+ "TC001", # typing-only-first-party-import
161
+ "TC002", # typing-only-third-party-import
162
+ "TC003", # typing-only-standard-library-import
163
+ "TC006", # runtime-cast-value
164
+ "TD001", # invalid-todo-tag
165
+ "TD002", # missing-todo-author
166
+ "TD003", # missing-todo-link
167
+ "TID252", # relative-imports
168
+ "TRY003" # raise-vanilla-args
169
+ ]
170
+ select = ["ALL"]
171
+
172
+ [tool.ruff.lint.per-file-ignores]
173
+ "*.ipynb" = [
174
+ "ANN", # flake8-annotations
175
+ "ARG001", # unused-function-argument
176
+ "D", # pydocstyle
177
+ "E402", # module-import-not-at-top-of-file
178
+ "E501", # line-too-long
179
+ "F821", # undefined-name
180
+ "FBT003", # boolean-positional-value-in-call
181
+ "N816", # mixed-case-variable-in-global-scope
182
+ "NPY", # numpy (allow legacy RNG calls in notebooks)
183
+ "PLC2401", # non-ascii-name
184
+ "S101", # assert
185
+ "SLF001", # private-member-access
186
+ "T201" # print
187
+ ]
188
+ "scripts/*.py" = [
189
+ "ANN", # flake8-annotations
190
+ "ARG001", # unused-function-argument
191
+ "D", # pydocstyle
192
+ "E402", # module-import-not-at-top-of-file
193
+ "E501", # line-too-long
194
+ "F821", # undefined-name
195
+ "FBT003", # boolean-positional-value-in-call
196
+ "INP001", # implicit-namespace-package
197
+ "N816", # mixed-case-variable-in-global-scope
198
+ "PLC2401", # non-ascii-name
199
+ "S101", # assert
200
+ "SLF001", # private-member-access
201
+ "T201" # print
202
+ ]
203
+ "tests/**/*.py" = [
204
+ "ANN", # flake8-annotations
205
+ "D", # pydocstyle
206
+ "INP001", # implicit-namespace-package
207
+ "PLC0415", # allow imports inside tests
208
+ "PT011", # pytest-raises-too-broad
209
+ "S101", # assert
210
+ "T201" # print
211
+ ]
212
+
213
+ [tool.ruff.lint.pydocstyle]
214
+ convention = "google"
215
+
216
+ [tool.setuptools.packages.find]
217
+ where = ["src"]
gsim-0.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ """GSIM - GDSFactory+ Simulation Tools.
2
+
3
+ This package provides APIs and client SDKs for accessing simulation tools
4
+ of gdsfactory+.
5
+
6
+ Currently includes:
7
+ - palace: Palace EM simulation API
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ __version__ = "0.0.0"
13
+
14
+ __all__ = [
15
+ "__version__",
16
+ ]
@@ -0,0 +1,169 @@
1
+ """GDSFactory+ cloud simulation interface.
2
+
3
+ This module provides an interface to run simulations on
4
+ the GDSFactory+ cloud infrastructure.
5
+
6
+ Usage:
7
+ from gsim import gcloud
8
+
9
+ # Run simulation (uploads, starts, waits, downloads)
10
+ results = gcloud.run_simulation("./sim", job_type="palace")
11
+
12
+ # Or use solver-specific wrappers:
13
+ from gsim import palace as pa
14
+ results = pa.run_simulation("./sim")
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import zipfile
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING
22
+
23
+ from gdsfactoryplus import sim # type: ignore[import-untyped]
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Callable
27
+ from typing import Literal
28
+
29
+
30
+ def _get_job_definition(job_type: str):
31
+ """Get JobDefinition enum value by name."""
32
+ job_type_upper = job_type.upper()
33
+ if not hasattr(sim.JobDefinition, job_type_upper):
34
+ valid = [e.name for e in sim.JobDefinition]
35
+ raise ValueError(f"Unknown job type '{job_type}'. Valid types: {valid}")
36
+ return getattr(sim.JobDefinition, job_type_upper)
37
+
38
+
39
+ def upload_simulation_dir(input_dir: str | Path, job_type: str):
40
+ """Zip all files in a directory and upload for simulation.
41
+
42
+ Args:
43
+ input_dir: Directory containing simulation files
44
+ job_type: Simulation type (e.g., "palace")
45
+
46
+ Returns:
47
+ PreJob object from gdsfactoryplus
48
+ """
49
+ input_dir = Path(input_dir)
50
+ zip_path = Path("_gsim_upload.zip")
51
+
52
+ try:
53
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_STORED) as zf:
54
+ for file in input_dir.rglob("*"):
55
+ if file.is_file():
56
+ zf.write(file, arcname=file.relative_to(input_dir))
57
+
58
+ job_definition = _get_job_definition(job_type)
59
+ pre_job = sim.upload_simulation(path=zip_path, job_definition=job_definition)
60
+ finally:
61
+ if zip_path.exists():
62
+ zip_path.unlink()
63
+
64
+ return pre_job
65
+
66
+
67
+ def run_simulation(
68
+ output_dir: str | Path,
69
+ job_type: Literal["palace"] = "palace",
70
+ verbose: bool = True,
71
+ on_started: Callable | None = None,
72
+ ) -> dict[str, Path]:
73
+ """Run a simulation on GDSFactory+ cloud.
74
+
75
+ This function handles the complete workflow:
76
+ 1. Uploads simulation files
77
+ 2. Starts the simulation job
78
+ 3. Waits for completion
79
+ 4. Downloads results
80
+
81
+ Args:
82
+ output_dir: Directory containing the simulation files
83
+ job_type: Type of simulation (default: "palace")
84
+ verbose: Print progress messages (default True)
85
+ on_started: Optional callback called with job object when simulation starts
86
+
87
+ Returns:
88
+ Dict mapping result filename to local Path.
89
+
90
+ Raises:
91
+ RuntimeError: If simulation fails
92
+
93
+ Example:
94
+ >>> results = gcloud.run_simulation("./sim", job_type="palace")
95
+ Uploading simulation... done
96
+ Job started: palace-abc123
97
+ Waiting for completion... done (2m 34s)
98
+ Downloading results... done
99
+ """
100
+ output_dir = Path(output_dir)
101
+
102
+ if not output_dir.exists():
103
+ raise FileNotFoundError(f"Output directory not found: {output_dir}")
104
+
105
+ # Upload
106
+ if verbose:
107
+ print("Uploading simulation... ", end="", flush=True) # noqa: T201
108
+
109
+ pre_job = upload_simulation_dir(output_dir, job_type)
110
+
111
+ if verbose:
112
+ print("done") # noqa: T201
113
+
114
+ # Start
115
+ job = sim.start_simulation(pre_job)
116
+
117
+ if verbose:
118
+ print(f"Job started: {job.job_name}") # noqa: T201
119
+
120
+ if on_started:
121
+ on_started(job)
122
+
123
+ # Wait
124
+ finished_job = sim.wait_for_simulation(job)
125
+
126
+ # Check status
127
+ if finished_job.exit_code != 0:
128
+ raise RuntimeError(
129
+ f"Simulation failed with exit code {finished_job.exit_code}. "
130
+ f"Status: {finished_job.status.value}"
131
+ )
132
+
133
+ # Download
134
+ results = sim.download_results(finished_job)
135
+
136
+ if verbose and results:
137
+ first_path = next(iter(results.values()))
138
+ download_dir = first_path.parent
139
+ print(f"Downloaded {len(results)} files to {download_dir}") # noqa: T201
140
+
141
+ return results
142
+
143
+
144
+ def print_job_summary(job) -> None:
145
+ """Print a formatted summary of a simulation job.
146
+
147
+ Args:
148
+ job: Job object from gdsfactoryplus
149
+ """
150
+ if job.started_at and job.finished_at:
151
+ delta = job.finished_at - job.started_at
152
+ minutes, seconds = divmod(int(delta.total_seconds()), 60)
153
+ duration = f"{minutes}m {seconds}s"
154
+ else:
155
+ duration = "N/A"
156
+
157
+ size_kb = job.output_size_bytes / 1024
158
+ size_str = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb / 1024:.2f} MB"
159
+ files = list(job.download_urls.keys()) if job.download_urls else []
160
+
161
+ print(f"{'Job:':<12} {job.job_name}") # noqa: T201
162
+ print(f"{'Status:':<12} {job.status.value} (exit {job.exit_code})") # noqa: T201
163
+ print(f"{'Duration:':<12} {duration}") # noqa: T201
164
+ mem_gb = job.requested_memory_mb // 1024
165
+ print(f"{'Resources:':<12} {job.requested_cpu} CPU / {mem_gb} GB") # noqa: T201
166
+ print(f"{'Output:':<12} {size_str}") # noqa: T201
167
+ print(f"{'Files:':<12} {len(files)} files") # noqa: T201
168
+ for f in files:
169
+ print(f" - {f}") # noqa: T201