Coverage for klayout_pex/util/unit_formatter.py: 79%
34 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 20:14 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 20:14 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
25from typing import Optional
28# NGSPICE manual, Table 2.1 "Ngspice scale factors"
29prefix_scales = [
30 ('T', 1e12),
31 ('G', 1e9),
32 ('Meg', 1e6),
33 ('K', 1e3),
34 ('', 1.0),
35 ('mil', 25.4e-6),
36 ('m', 1e-3),
37 ('u', 1e-6),
38 ('n', 1e-9),
39 ('p', 1e-12),
40 ('f', 1e-15),
41 ('a', 1e-18),
42]
45valid_prefixes = {p for p, _ in prefix_scales}
48def format_spice_number(value: float, force_prefix: Optional[str] = None) -> str:
49 """
50 Format a spice number into NGSPICE-compatible string.
52 Args:
53 value: capacitance in farads (can be negative or zero).
54 force_prefix: optional forced prefix (must match NGSPICE case, e.g., 'a','f','p','n','u','m','K','Meg','G','T').
56 Returns:
57 NGSPICE-compatible string like '4.7n', '1p', '2.2u', '-33f', or '0'.
58 """
59 if value == 0:
60 return "0"
62 if force_prefix is not None:
63 if force_prefix not in valid_prefixes:
64 raise ValueError(f"Invalid prefix '{force_prefix}'. Must be one of {sorted(valid_prefixes)}")
65 scale = dict(prefix_scales)[force_prefix]
66 scaled = value / scale
67 # Force decimal notation for NGSPICE (no scientific notation)
68 if scaled.is_integer():
69 return f"{int(scaled)}{force_prefix}"
70 else:
71 return f"{scaled:.6g}{force_prefix}"
73 chosen_prefix = ''
74 chosen_scaled = value
76 for i, (prefix, scale) in enumerate(prefix_scales):
77 scaled = value / scale
78 if 1 <= abs(scaled) < 1000:
79 chosen_prefix = prefix
80 chosen_scaled = scaled
81 break
82 elif abs(scaled) >= 1000:
83 # bump up to next larger prefix if possible
84 if i == 0:
85 chosen_prefix = prefix
86 chosen_scaled = scaled
87 else:
88 next_prefix, next_scale = prefix_scales[i - 1]
89 chosen_prefix = next_prefix
90 chosen_scaled = value / next_scale
91 break
92 else:
93 # too small, pick smallest prefix
94 chosen_prefix, chosen_scaled = prefix_scales[-1][0], value / prefix_scales[-1][1]
96 # Avoid scientific notation for NGSPICE
97 if chosen_scaled.is_integer():
98 return f"{int(chosen_scaled)}{chosen_prefix}"
99 else:
100 return f"{chosen_scaled:.6g}{chosen_prefix}"