consync 0.1.0__py3-none-any.whl

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.
consync/precision.py ADDED
@@ -0,0 +1,148 @@
1
+ """Precision-preserving number formatting for IEEE 754 doubles.
2
+
3
+ The core problem: hardware constants have 15-17 significant digits.
4
+ Excel stores them as IEEE 754 doubles. When writing to C/Verilog/Python,
5
+ we must preserve ALL significant digits to guarantee round-trip fidelity:
6
+
7
+ parse(format(x)) == x
8
+
9
+ Default: 17 significant digits (maximum for double precision).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import math
15
+ from decimal import Decimal
16
+
17
+
18
+ def format_float(value: float, precision: int = 17) -> str:
19
+ """Format a float preserving significant digits.
20
+
21
+ Uses Python's `g` format which automatically chooses between
22
+ fixed-point and scientific notation for best readability.
23
+
24
+ Args:
25
+ value: The float to format.
26
+ precision: Number of significant digits (1-21). Default 17.
27
+
28
+ Returns:
29
+ String representation with full precision.
30
+
31
+ Examples:
32
+ >>> format_float(1.20029384729384)
33
+ '1.20029384729384'
34
+ >>> format_float(0.00000047832940)
35
+ '4.783294e-07'
36
+ >>> format_float(100029.4829183)
37
+ '100029.4829183'
38
+ >>> format_float(4700.0)
39
+ '4700'
40
+ """
41
+ if precision < 1:
42
+ precision = 1
43
+ if precision > 21:
44
+ precision = 21
45
+
46
+ if math.isnan(value):
47
+ return "NAN"
48
+ if math.isinf(value):
49
+ return "INF" if value > 0 else "-INF"
50
+
51
+ formatted = f"{value:.{precision}g}"
52
+
53
+ # Ensure no trailing zeros that add false precision,
54
+ # but keep at least one digit after decimal for floats
55
+ return formatted
56
+
57
+
58
+ def format_c_double(value: float, precision: int = 17) -> str:
59
+ """Format a float for C/C++ const double declaration.
60
+
61
+ Always produces a string that, when parsed by a C compiler,
62
+ yields the exact same IEEE 754 bit pattern.
63
+
64
+ Examples:
65
+ >>> format_c_double(4.7832940e-07)
66
+ '4.7832940000000000e-07'
67
+ >>> format_c_double(4700.0)
68
+ '4700'
69
+ >>> format_c_double(1.20029384729384)
70
+ '1.20029384729384'
71
+ """
72
+ return format_float(value, precision)
73
+
74
+
75
+ def format_scientific(value: float, precision: int = 17) -> str:
76
+ """Always use scientific notation (for Verilog/VHDL real parameters).
77
+
78
+ Examples:
79
+ >>> format_scientific(4700.0)
80
+ '4.7e+03'
81
+ >>> format_scientific(0.00000047832940)
82
+ '4.783294e-07'
83
+ """
84
+ if precision < 1:
85
+ precision = 1
86
+ return f"{value:.{precision}e}"
87
+
88
+
89
+ def format_fixed(value: float, decimal_places: int = 14) -> str:
90
+ """Fixed-point notation with explicit decimal places.
91
+
92
+ Examples:
93
+ >>> format_fixed(1.20029384729384, 14)
94
+ '1.20029384729384'
95
+ """
96
+ return f"{value:.{decimal_places}f}".rstrip("0").rstrip(".")
97
+
98
+
99
+ def parse_number(text: str) -> float | int:
100
+ """Parse a numeric string, preserving int vs float distinction.
101
+
102
+ Handles scientific notation, underscores (Rust/Verilog style),
103
+ and common suffixes.
104
+
105
+ Examples:
106
+ >>> parse_number("4.7832940e-07")
107
+ 4.783294e-07
108
+ >>> parse_number("4700")
109
+ 4700
110
+ >>> parse_number("1_000_000")
111
+ 1000000
112
+ >>> parse_number("0xFF")
113
+ 255
114
+ """
115
+ text = text.strip().replace("_", "")
116
+
117
+ # Hex literals
118
+ if text.lower().startswith("0x"):
119
+ return int(text, 16)
120
+
121
+ # Binary literals
122
+ if text.lower().startswith("0b"):
123
+ return int(text, 2)
124
+
125
+ # Try int first
126
+ try:
127
+ val = int(text)
128
+ return val
129
+ except ValueError:
130
+ pass
131
+
132
+ # Then float
133
+ return float(text)
134
+
135
+
136
+ def significant_digits(value: float) -> int:
137
+ """Count the significant digits in a float's representation.
138
+
139
+ Uses Decimal to get the shortest representation that round-trips.
140
+ """
141
+ if value == 0:
142
+ return 1
143
+ d = Decimal(str(value))
144
+ # sign, digits, exponent
145
+ _, digits, _ = d.as_tuple()
146
+ # Strip trailing zeros
147
+ stripped = str(int("".join(str(d) for d in digits))).rstrip("0")
148
+ return len(stripped) if stripped else 1
@@ -0,0 +1,49 @@
1
+ """Renderer registry — maps format names to renderer functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Callable
7
+
8
+ from consync.models import Constant
9
+
10
+ # Type for renderer functions: (constants, filepath, **kwargs) -> None
11
+ RendererFunc = Callable[..., None]
12
+
13
+ _REGISTRY: dict[str, RendererFunc] = {}
14
+
15
+
16
+ def register(name: str):
17
+ """Decorator to register a renderer function."""
18
+ def decorator(func: RendererFunc) -> RendererFunc:
19
+ _REGISTRY[name] = func
20
+ return func
21
+ return decorator
22
+
23
+
24
+ def get_renderer(format_name: str) -> RendererFunc:
25
+ """Get a renderer by format name."""
26
+ if format_name not in _REGISTRY:
27
+ available = ", ".join(sorted(_REGISTRY.keys()))
28
+ raise ValueError(
29
+ f"Unknown renderer format '{format_name}'. Available: {available}"
30
+ )
31
+ return _REGISTRY[format_name]
32
+
33
+
34
+ def list_renderers() -> list[str]:
35
+ """List all registered renderer format names."""
36
+ return sorted(_REGISTRY.keys())
37
+
38
+
39
+ # Import all renderer modules to trigger registration
40
+ from consync.renderers import ( # noqa: E402, F401
41
+ c_header,
42
+ csharp,
43
+ csv_renderer,
44
+ python_const,
45
+ rust_const,
46
+ verilog,
47
+ vhdl,
48
+ json_renderer,
49
+ )
@@ -0,0 +1,222 @@
1
+ """C/C++ header renderer — generates const declarations or #define macros.
2
+
3
+ Supports:
4
+ - `const double` (default) or `#define` output style
5
+ - `static const` for multi-TU safety
6
+ - Typed integers: uint32_t / int32_t / uint16_t / int16_t / uint8_t / int8_t
7
+ - Array constants: static const int X[] = {1, 2, 3};
8
+ - Configurable precision for doubles/floats
9
+
10
+ Output examples:
11
+
12
+ Style "const" (default):
13
+ static const uint32_t R_PULLUP = 4706U; /* Ohm | I2C pull-up */
14
+ static const double R_SENSE = 1.9999999999910001; /* Ohm | Sense R */
15
+ static const int THRESHOLDS[] = {50, 100, 150, 200, 250}; /* bar | Brake thresholds */
16
+
17
+ Style "define":
18
+ #define R_PULLUP (4706U) /* Ohm | I2C pull-up */
19
+ #define R_SENSE (1.9999999999910001) /* Ohm | Sense R */
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+
27
+ from consync.models import Constant, MappingConfig
28
+ from consync.precision import format_c_double
29
+ from consync.renderers import register
30
+
31
+
32
+ def _c_type_for_int(value: int, typed_ints: bool) -> tuple[str, str]:
33
+ """Determine C type and suffix for an integer value.
34
+
35
+ Returns (type_name, literal_suffix).
36
+ """
37
+ if not typed_ints:
38
+ return ("int", "")
39
+
40
+ if value < 0:
41
+ if -128 <= value <= 127:
42
+ return ("int8_t", "")
43
+ elif -32768 <= value <= 32767:
44
+ return ("int16_t", "")
45
+ elif -2147483648 <= value <= 2147483647:
46
+ return ("int32_t", "")
47
+ else:
48
+ return ("int64_t", "LL")
49
+ else:
50
+ if value <= 255:
51
+ return ("uint8_t", "U")
52
+ elif value <= 65535:
53
+ return ("uint16_t", "U")
54
+ elif value <= 4294967295:
55
+ return ("uint32_t", "U")
56
+ else:
57
+ return ("uint64_t", "ULL")
58
+
59
+
60
+ def _c_type_for_float(value: float) -> str:
61
+ """Determine C float type — always double for full precision."""
62
+ return "double"
63
+
64
+
65
+ @register("c_header")
66
+ def render_c_header(
67
+ constants: list[Constant],
68
+ filepath: str | Path,
69
+ *,
70
+ config: MappingConfig | None = None,
71
+ **kwargs,
72
+ ) -> None:
73
+ """Render constants as a C/C++ header file.
74
+
75
+ Args:
76
+ constants: Constants to write.
77
+ filepath: Output .h file path.
78
+ config: Mapping config (for precision, header_guard, output_style, etc.)
79
+ """
80
+ filepath = Path(filepath)
81
+ precision = config.precision if config else kwargs.get("precision", 17)
82
+ source_name = config.source if config else kwargs.get("source", "unknown")
83
+ output_style = (config.output_style if config else kwargs.get("output_style", "const")).lower()
84
+ static = config.static_const if config else kwargs.get("static_const", False)
85
+ typed_ints = config.typed_ints if config else kwargs.get("typed_ints", True)
86
+
87
+ # Determine header guard
88
+ guard = ""
89
+ if config and config.header_guard:
90
+ guard = config.header_guard
91
+ else:
92
+ guard = filepath.stem.upper().replace("-", "_").replace(".", "_") + "_H"
93
+
94
+ # Prefix handling
95
+ prefix = config.prefix if config else kwargs.get("prefix", "")
96
+
97
+ # Calculate alignment
98
+ names = [prefix + c.name for c in constants]
99
+ name_width = max((len(n) for n in names), default=20) + 2
100
+
101
+ # Qualifier for const style
102
+ qualifier = "static const" if static else "const"
103
+
104
+ # Include for stdint types
105
+ needs_stdint = typed_ints and any(
106
+ isinstance(c.value, int) or
107
+ (isinstance(c.value, list) and c.value and isinstance(c.value[0], int))
108
+ for c in constants
109
+ )
110
+
111
+ # Build header
112
+ lines = [
113
+ f"/* AUTO-GENERATED by consync — do not edit by hand.",
114
+ f" * Source: {source_name}",
115
+ f" * Synced: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
116
+ f" * Regenerate: consync sync",
117
+ f" */",
118
+ f"",
119
+ f"#ifndef {guard}",
120
+ f"#define {guard}",
121
+ f"",
122
+ ]
123
+
124
+ if needs_stdint and output_style == "const":
125
+ lines.append("#include <stdint.h>")
126
+ lines.append("")
127
+
128
+ # Build constant declarations
129
+ for c in constants:
130
+ name = prefix + c.name
131
+ if config and config.uppercase_names:
132
+ name = name.upper()
133
+
134
+ # Build comment
135
+ parts = [p for p in (c.unit, c.description) if p]
136
+ comment = f" /* {' | '.join(parts)} */" if parts else ""
137
+
138
+ # === Array values ===
139
+ if isinstance(c.value, list):
140
+ _render_array_const(lines, c, name, qualifier, typed_ints, precision, comment)
141
+ continue
142
+
143
+ # === Scalar values ===
144
+ # Format value and determine type
145
+ if isinstance(c.value, int):
146
+ c_type, suffix = _c_type_for_int(c.value, typed_ints)
147
+ val_str = str(c.value) + suffix
148
+ else:
149
+ c_type = _c_type_for_float(c.value)
150
+ val_str = format_c_double(c.value, precision)
151
+ suffix = ""
152
+
153
+ # Emit line based on output style
154
+ if output_style == "define":
155
+ lines.append(f"#define {name:<{name_width}} ({val_str}){comment}")
156
+ else:
157
+ type_width = max(len(_get_c_type(c2, typed_ints)) for c2 in constants
158
+ if not isinstance(c2.value, list)) if any(
159
+ not isinstance(c2.value, list) for c2 in constants) else len(c_type)
160
+ lines.append(
161
+ f"{qualifier} {c_type:<{type_width}} {name:<{name_width}} = {val_str};{comment}"
162
+ )
163
+
164
+ lines += ["", f"#endif /* {guard} */", ""]
165
+
166
+ filepath.parent.mkdir(parents=True, exist_ok=True)
167
+ filepath.write_text("\n".join(lines), encoding="utf-8")
168
+
169
+
170
+ def _get_c_type(c: "Constant", typed_ints: bool) -> str:
171
+ """Get the C type string for a scalar constant."""
172
+ if isinstance(c.value, int):
173
+ return _c_type_for_int(c.value, typed_ints)[0]
174
+ return _c_type_for_float(c.value)
175
+
176
+
177
+ def _render_array_const(
178
+ lines: list[str],
179
+ c: "Constant",
180
+ name: str,
181
+ qualifier: str,
182
+ typed_ints: bool,
183
+ precision: int,
184
+ comment: str,
185
+ ) -> None:
186
+ """Render an array constant as: static const int X[] = {1, 2, 3};"""
187
+ from consync.models import ConstantType
188
+
189
+ arr = c.value
190
+ if not arr:
191
+ return
192
+
193
+ first = arr[0]
194
+ if isinstance(first, int):
195
+ # Use typed ints based on max value in array
196
+ max_val = max(arr)
197
+ min_val = min(arr)
198
+ if min_val < 0:
199
+ c_type = _c_type_for_int(min_val, typed_ints)[0]
200
+ # Ensure type fits max too
201
+ if typed_ints:
202
+ c_type2 = _c_type_for_int(max_val, typed_ints)[0]
203
+ # Pick the wider signed type
204
+ if "64" in c_type or "64" in c_type2:
205
+ c_type = "int64_t"
206
+ elif "32" in c_type or "32" in c_type2:
207
+ c_type = "int32_t"
208
+ elif "16" in c_type or "16" in c_type2:
209
+ c_type = "int16_t"
210
+ else:
211
+ c_type = _c_type_for_int(max_val, typed_ints)[0]
212
+ vals = ", ".join(str(v) for v in arr)
213
+ elif isinstance(first, float):
214
+ c_type = "double"
215
+ vals = ", ".join(format_c_double(v, precision) for v in arr)
216
+ else:
217
+ # String arrays → not directly supported in C, skip
218
+ return
219
+
220
+ lines.append(
221
+ f"{qualifier} {c_type} {name}[] = {{{vals}}};{comment}"
222
+ )
@@ -0,0 +1,174 @@
1
+ """C# renderer — generates a static class with typed constants.
2
+
3
+ Output example:
4
+ // AUTO-GENERATED by consync — do not edit by hand.
5
+ namespace HardwareConstants
6
+ {
7
+ public static class Constants
8
+ {
9
+ /// <summary>Current sense resistor (Ohm)</summary>
10
+ public const double R_SENSE = 1.9999999999910001;
11
+
12
+ /// <summary>I2C pull-up resistor (Ohm)</summary>
13
+ public const uint R_PULLUP = 4706;
14
+ }
15
+ }
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+
23
+ from consync.models import Constant, MappingConfig
24
+ from consync.precision import format_float
25
+ from consync.renderers import register
26
+
27
+
28
+ def _csharp_type(value: int | float | str, typed_ints: bool = True) -> tuple[str, str]:
29
+ """Determine C# type and optional suffix for a value.
30
+
31
+ Returns (type_name, literal_suffix).
32
+ """
33
+ if isinstance(value, int):
34
+ if not typed_ints:
35
+ return ("int", "")
36
+ if value < 0:
37
+ if -2147483648 <= value <= 2147483647:
38
+ return ("int", "")
39
+ else:
40
+ return ("long", "L")
41
+ else:
42
+ if value <= 2147483647:
43
+ return ("int", "")
44
+ elif value <= 4294967295:
45
+ return ("uint", "U")
46
+ else:
47
+ return ("ulong", "UL")
48
+ elif isinstance(value, float):
49
+ return ("double", "")
50
+ else:
51
+ return ("string", "")
52
+
53
+
54
+ @register("csharp")
55
+ def render_csharp(
56
+ constants: list[Constant],
57
+ filepath: str | Path,
58
+ *,
59
+ config: MappingConfig | None = None,
60
+ **kwargs,
61
+ ) -> None:
62
+ """Render constants as a C# static class.
63
+
64
+ Args:
65
+ constants: Constants to write.
66
+ filepath: Output .cs file path.
67
+ config: Mapping config (for precision, namespace, etc.)
68
+ """
69
+ filepath = Path(filepath)
70
+ precision = config.precision if config else kwargs.get("precision", 17)
71
+ source_name = config.source if config else kwargs.get("source", "unknown")
72
+ namespace = (config.namespace if config else kwargs.get("namespace", "")) or "Constants"
73
+ class_name = (config.module_name if config else kwargs.get("module_name", "")) or "HwConstants"
74
+ typed_ints = config.typed_ints if config else kwargs.get("typed_ints", True)
75
+ prefix = config.prefix if config else kwargs.get("prefix", "")
76
+
77
+ lines = [
78
+ f"// AUTO-GENERATED by consync — do not edit by hand.",
79
+ f"// Source: {source_name}",
80
+ f"// Synced: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
81
+ f"// Regenerate: consync sync",
82
+ f"",
83
+ f"namespace {namespace}",
84
+ f"{{",
85
+ f" public static class {class_name}",
86
+ f" {{",
87
+ ]
88
+
89
+ for c in constants:
90
+ name = prefix + c.name
91
+ if config and config.uppercase_names:
92
+ name = name.upper()
93
+
94
+ # XML doc comment
95
+ doc_parts = [p for p in (c.description, f"({c.unit})" if c.unit else "") if p]
96
+ if doc_parts:
97
+ doc = " ".join(doc_parts)
98
+ lines.append(f" /// <summary>{doc}</summary>")
99
+
100
+ # === Array values ===
101
+ if isinstance(c.value, list):
102
+ _render_csharp_array(lines, c, name, typed_ints, precision)
103
+ lines.append("")
104
+ continue
105
+
106
+ # === Scalar values ===
107
+ cs_type, suffix = _csharp_type(c.value, typed_ints)
108
+
109
+ # Format value
110
+ if isinstance(c.value, int):
111
+ val_str = str(c.value) + suffix
112
+ elif isinstance(c.value, float):
113
+ val_str = format_float(c.value, precision)
114
+ else:
115
+ val_str = f'"{c.value}"'
116
+
117
+ lines.append(f" public const {cs_type} {name} = {val_str};")
118
+ lines.append("")
119
+
120
+ # Remove trailing blank line before closing braces
121
+ if lines and lines[-1] == "":
122
+ lines.pop()
123
+
124
+ lines += [
125
+ f" }}",
126
+ f"}}",
127
+ "",
128
+ ]
129
+
130
+ filepath.parent.mkdir(parents=True, exist_ok=True)
131
+ filepath.write_text("\n".join(lines), encoding="utf-8")
132
+
133
+
134
+ def _render_csharp_array(
135
+ lines: list[str],
136
+ c: "Constant",
137
+ name: str,
138
+ typed_ints: bool,
139
+ precision: int,
140
+ ) -> None:
141
+ """Render an array constant as: public static readonly int[] X = { 1, 2, 3 };"""
142
+ arr = c.value
143
+ if not arr:
144
+ return
145
+
146
+ first = arr[0]
147
+ if isinstance(first, int):
148
+ cs_type = "int"
149
+ if typed_ints:
150
+ max_val = max(arr)
151
+ min_val = min(arr)
152
+ if min_val < 0:
153
+ if min_val >= -2147483648 and max_val <= 2147483647:
154
+ cs_type = "int"
155
+ else:
156
+ cs_type = "long"
157
+ else:
158
+ if max_val <= 2147483647:
159
+ cs_type = "int"
160
+ elif max_val <= 4294967295:
161
+ cs_type = "uint"
162
+ else:
163
+ cs_type = "ulong"
164
+ vals = ", ".join(str(v) for v in arr)
165
+ elif isinstance(first, float):
166
+ cs_type = "double"
167
+ vals = ", ".join(format_float(v, precision) for v in arr)
168
+ else:
169
+ cs_type = "string"
170
+ vals = ", ".join(f'"{v}"' for v in arr)
171
+
172
+ lines.append(
173
+ f" public static readonly {cs_type}[] {name} = {{ {vals} }};"
174
+ )
@@ -0,0 +1,46 @@
1
+ """CSV renderer — writes constants to a CSV file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ from pathlib import Path
7
+
8
+ from consync.models import Constant, MappingConfig
9
+ from consync.renderers import register
10
+
11
+
12
+ @register("csv")
13
+ def render_csv(
14
+ constants: list[Constant],
15
+ filepath: str | Path,
16
+ *,
17
+ config: MappingConfig | None = None,
18
+ **kwargs,
19
+ ) -> None:
20
+ """Render constants as a CSV file with headers Name, Value, Unit, Description."""
21
+ filepath = Path(filepath)
22
+ filepath.parent.mkdir(parents=True, exist_ok=True)
23
+
24
+ delimiter = ","
25
+ if filepath.suffix.lower() == ".tsv":
26
+ delimiter = "\t"
27
+
28
+ with filepath.open("w", newline="", encoding="utf-8") as f:
29
+ writer = csv.writer(f, delimiter=delimiter)
30
+ writer.writerow(["Name", "Value", "Unit", "Description"])
31
+ for c in constants:
32
+ # Format value: arrays use pipe separator, floats get precision
33
+ if isinstance(c.value, list):
34
+ if c.value and isinstance(c.value[0], float):
35
+ from consync.precision import format_float
36
+ precision = config.precision if config else 17
37
+ val_str = "|".join(format_float(v, precision) for v in c.value)
38
+ else:
39
+ val_str = "|".join(str(v) for v in c.value)
40
+ elif isinstance(c.value, float):
41
+ from consync.precision import format_float
42
+ precision = config.precision if config else 17
43
+ val_str = format_float(c.value, precision)
44
+ else:
45
+ val_str = str(c.value)
46
+ writer.writerow([c.name, val_str, c.unit or "", c.description or ""])