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

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# 

24 

25from typing import Optional 

26 

27 

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] 

43 

44 

45valid_prefixes = {p for p, _ in prefix_scales} 

46 

47 

48def format_spice_number(value: float, force_prefix: Optional[str] = None) -> str: 

49 """ 

50 Format a spice number into NGSPICE-compatible string. 

51 

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'). 

55 

56 Returns: 

57 NGSPICE-compatible string like '4.7n', '1p', '2.2u', '-33f', or '0'. 

58 """ 

59 if value == 0: 

60 return "0" 

61 

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}" 

72 

73 chosen_prefix = '' 

74 chosen_scaled = value 

75 

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] 

95 

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}"