analog-daddy 3.0.1.dev1__tar.gz → 3.0.1.dev2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: analog-daddy
3
- Version: 3.0.1.dev1
3
+ Version: 3.0.1.dev2
4
4
  Summary: This library is an attempt to make transistor sizing for Analog Design less painful. Additionally, provides helper utilities to minimize churn and errors in repetative tasks.
5
5
  Author: Siddhant Laddha
6
6
  Author-email: Siddhant Laddha <25705822+siddhantladdha@users.noreply.github.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "analog-daddy"
3
- version = "3.0.1.dev1"
3
+ version = "3.0.1.dev2"
4
4
  description = "This library is an attempt to make transistor sizing for Analog Design less painful. Additionally, provides helper utilities to minimize churn and errors in repetative tasks."
5
5
  readme = "README.md"
6
6
  requires-python = ">= 3.12"
@@ -46,6 +46,7 @@ analog_daddy = "analog_daddy.cli:app"
46
46
  [dependency-groups]
47
47
  dev = [
48
48
  "format-docstring",
49
+ "hypothesis",
49
50
  "pytest",
50
51
  "ruff",
51
52
  "ty",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "analog-daddy"
3
- version = "3.0.1.dev1"
3
+ version = "3.0.1.dev2"
4
4
  description = "This library is an attempt to make transistor sizing for Analog Design less painful. Additionally, provides helper utilities to minimize churn and errors in repetative tasks."
5
5
  readme = "README.md"
6
6
  requires-python = ">= 3.12"
@@ -44,6 +44,7 @@ analog_daddy = "analog_daddy.cli:app"
44
44
  [dependency-groups]
45
45
  dev = [
46
46
  "format-docstring",
47
+ "hypothesis",
47
48
  "pytest",
48
49
  "ruff",
49
50
  "ty",
@@ -0,0 +1,5 @@
1
+ """Top-level package for Analog Daddy."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("analog-daddy")
@@ -14,6 +14,7 @@ import marimo as mo
14
14
  import polars as pl
15
15
  import polars.selectors as cs
16
16
  from rich.console import Console, RenderableType
17
+ from wigglystuff import CopyToClipboard
17
18
 
18
19
  from analog_daddy.utils import render
19
20
 
@@ -121,3 +122,35 @@ def rich_md(
121
122
  console = Console(file=capture, **console_kwargs)
122
123
  console.print(*renderables, **print_kwargs)
123
124
  return mo.Html(console.export_html(**export_html_kwargs))
125
+
126
+
127
+ def df_repr_docstr_clip(
128
+ df: pl.DataFrame, max_rows: int = 3
129
+ ) -> CopyToClipboard:
130
+ """
131
+ Copy the repr of a polars DataFrame to the clipboard.
132
+
133
+ Parameters
134
+ ----------
135
+ df : pl.DataFrame
136
+ The DataFrame to copy the repr of.
137
+ max_rows : int, default=3
138
+ The maximum number of rows to include in the repr.
139
+
140
+ Returns
141
+ -------
142
+ CopyToClipboard
143
+ A CopyToClipboard object containing the repr of the DataFrame.
144
+
145
+ Examples
146
+ --------
147
+ >>> a = pl.DataFrame(
148
+ ... {"x": [9.0, 11.0], "foo": [10.0, 10.0], "bar": [5.0, 5.0]},
149
+ ... )
150
+ >>> df_repr_docstr_clip(a).text_to_copy.startswith("shape: (2, 3)")
151
+ True
152
+ """
153
+
154
+ return CopyToClipboard(
155
+ text_to_copy=df.slice(offset=0, length=max_rows).__repr__(),
156
+ )
@@ -440,7 +440,51 @@ def spec(
440
440
  raise ValueError(f"bad spec: {spec}")
441
441
 
442
442
 
443
- def list_cast():
443
+ def list_cast() -> tuple[pl.Expr, pl.Expr]:
444
+ """
445
+ Cast list columns to list[str]
446
+
447
+ Uses ``render`` to convert numeric values and casts categorical
448
+ values to string.
449
+
450
+ Returns
451
+ -------
452
+ tuple[pl.Expr, pl.Expr]
453
+
454
+ Examples
455
+ --------
456
+ >>> (
457
+ ... df := pl.DataFrame(
458
+ ... {"a": [[1000, 2.5e-6]], "b": [["x", "y"]]},
459
+ ... schema={
460
+ ... "a": pl.List(pl.Float64),
461
+ ... "b": pl.List(pl.Categorical),
462
+ ... },
463
+ ... )
464
+ ... )
465
+ shape: (1, 2)
466
+ ┌─────────────────────┬────────────┐
467
+ │ a ┆ b │
468
+ │ --- ┆ --- │
469
+ │ list[f64] ┆ list[cat] │
470
+ ╞═════════════════════╪════════════╡
471
+ │ [1000.0, 0.0000025] ┆ ["x", "y"] │
472
+ └─────────────────────┴────────────┘
473
+ >>> out = df.with_columns(*list_cast())
474
+ >>> out
475
+ shape: (1, 2)
476
+ ┌────────────────┬────────────┐
477
+ │ a ┆ b │
478
+ │ --- ┆ --- │
479
+ │ list[str] ┆ list[str] │
480
+ ╞════════════════╪════════════╡
481
+ │ ["1k", "2.5u"] ┆ ["x", "y"] │
482
+ └────────────────┴────────────┘
483
+ >>> out.schema.contains_dtype(pl.String, recursive=True)
484
+ True
485
+ >>> out.schema.contains_dtype(pl.Categorical, recursive=True)
486
+ False
487
+ """
444
488
  return (
445
489
  cs.list(cs.numeric()).list.eval(
446
490
  pl.element().map_elements(
@@ -452,7 +496,42 @@ def list_cast():
452
496
  )
453
497
 
454
498
 
455
- def list_render_unique(max_len: int = 3):
499
+ def list_render_unique(max_len: int = 3) -> pl.Expr:
500
+ """
501
+ Render unique values of list[str] columns as a comma-joined string.
502
+
503
+ Lists with more than ``max_len`` unique values are truncated and
504
+ suffixed with the count of omitted values. The ordering is not
505
+ deterministic since ``pl.Expr.unique`` doesn't maintain order.
506
+
507
+ Parameters
508
+ ----------
509
+ max_len : int, default=3
510
+ Maximum number of unique values to show before truncating.
511
+
512
+ Returns
513
+ -------
514
+ pl.Expr
515
+ Expression (over all list[str] columns) yielding a string.
516
+
517
+ Examples
518
+ --------
519
+ >>> (df := pl.DataFrame({"a": [["x", "x"], ["a", "b", "c", "d", "e"]]}))
520
+ shape: (2, 1)
521
+ ┌───────────────────┐
522
+ │ a │
523
+ │ --- │
524
+ │ list[str] │
525
+ ╞═══════════════════╡
526
+ │ ["x", "x"] │
527
+ │ ["a", "b", … "e"] │
528
+ └───────────────────┘
529
+ >>> out = df.select(list_render_unique(max_len=3))["a"].to_list()
530
+ >>> out[0]
531
+ 'x'
532
+ >>> out[1].endswith(" + 2 more")
533
+ True
534
+ """
456
535
  ulist = cs.list(cs.string()).list.unique().list
457
536
  n = ulist.len()
458
537
  head_n = ulist.head(max_len).list
@@ -22,15 +22,36 @@ def drop_all_null(df: pl.DataFrame) -> pl.DataFrame:
22
22
 
23
23
  Examples
24
24
  --------
25
- >>> df = pl.DataFrame(
26
- ... {
27
- ... "a": [None, None, None],
28
- ... "b": [1, 2, None],
29
- ... "c": ["x", "y", ""],
30
- ... }
25
+ >>> (
26
+ ... df := pl.DataFrame(
27
+ ... {
28
+ ... "a": [None, None, None],
29
+ ... "b": [1, 2, None],
30
+ ... "c": ["x", "y", ""],
31
+ ... }
32
+ ... )
31
33
  ... )
32
- >>> drop_all_null(df).columns
33
- ['b', 'c']
34
+ shape: (3, 3)
35
+ ┌──────┬──────┬─────┐
36
+ │ a ┆ b ┆ c │
37
+ │ --- ┆ --- ┆ --- │
38
+ │ null ┆ i64 ┆ str │
39
+ ╞══════╪══════╪═════╡
40
+ │ null ┆ 1 ┆ x │
41
+ │ null ┆ 2 ┆ y │
42
+ │ null ┆ null ┆ │
43
+ └──────┴──────┴─────┘
44
+ >>> drop_all_null(df)
45
+ shape: (3, 2)
46
+ ┌──────┬─────┐
47
+ │ b ┆ c │
48
+ │ --- ┆ --- │
49
+ │ i64 ┆ str │
50
+ ╞══════╪═════╡
51
+ │ 1 ┆ x │
52
+ │ 2 ┆ y │
53
+ │ null ┆ │
54
+ └──────┴─────┘
34
55
  """
35
56
  return df[[ser.name for ser in df if ser.null_count() != df.height]]
36
57
 
@@ -51,12 +72,24 @@ def filter_all_null(df: pl.DataFrame) -> pl.DataFrame:
51
72
 
52
73
  Examples
53
74
  --------
54
- >>> df = pl.DataFrame(
55
- ... {
56
- ... "a": [1, None, 3],
57
- ... "b": ["x", None, None],
58
- ... }
75
+ >>> (
76
+ ... df := pl.DataFrame(
77
+ ... {
78
+ ... "a": [1, None, 3],
79
+ ... "b": ["x", None, None],
80
+ ... }
81
+ ... )
59
82
  ... )
83
+ shape: (3, 2)
84
+ ┌──────┬──────┐
85
+ │ a ┆ b │
86
+ │ --- ┆ --- │
87
+ │ i64 ┆ str │
88
+ ╞══════╪══════╡
89
+ │ 1 ┆ x │
90
+ │ null ┆ null │
91
+ │ 3 ┆ null │
92
+ └──────┴──────┘
60
93
  >>> filter_all_null(df)
61
94
  shape: (2, 2)
62
95
  ┌─────┬──────┐
@@ -110,17 +143,30 @@ def scan_nested_params(
110
143
 
111
144
  Examples
112
145
  --------
113
- >>> df = pl.DataFrame(
114
- ... {
115
- ... "Point": [
116
- ... "Parameters: var1=-12k, var_b=1n",
117
- ... "1",
118
- ... "Parameters: var3=-12k",
119
- ... "2",
120
- ... ],
121
- ... "Value": [None, 10, 1, None],
122
- ... }
146
+ >>> (
147
+ ... df := pl.DataFrame(
148
+ ... {
149
+ ... "Point": [
150
+ ... "Parameters: var1=-12k, var_b=1n",
151
+ ... "1",
152
+ ... "Parameters: var3=-12k",
153
+ ... "2",
154
+ ... ],
155
+ ... "Value": [None, 10, 1, None],
156
+ ... }
157
+ ... )
123
158
  ... )
159
+ shape: (4, 2)
160
+ ┌─────────────────────────────────┬───────┐
161
+ │ Point ┆ Value │
162
+ │ --- ┆ --- │
163
+ │ str ┆ i64 │
164
+ ╞═════════════════════════════════╪═══════╡
165
+ │ Parameters: var1=-12k, var_b=1… ┆ null │
166
+ │ 1 ┆ 10 │
167
+ │ Parameters: var3=-12k ┆ 1 │
168
+ │ 2 ┆ null │
169
+ └─────────────────────────────────┴───────┘
124
170
  >>> scan_nested_params(df, scan_col="Point")
125
171
  ['var1', 'var_b']
126
172
  """
@@ -169,12 +215,24 @@ def explode_nested_params(
169
215
 
170
216
  Examples
171
217
  --------
172
- >>> df = pl.DataFrame(
173
- ... {
174
- ... "Point": ["Parameters: ds=0, gs=1", "1", "2"],
175
- ... "Value": [None, 10, 20],
176
- ... }
218
+ >>> (
219
+ ... df := pl.DataFrame(
220
+ ... {
221
+ ... "Point": ["Parameters: ds=0, gs=1", "1", "2"],
222
+ ... "Value": [None, 10, 20],
223
+ ... }
224
+ ... )
177
225
  ... )
226
+ shape: (3, 2)
227
+ ┌────────────────────────┬───────┐
228
+ │ Point ┆ Value │
229
+ │ --- ┆ --- │
230
+ │ str ┆ i64 │
231
+ ╞════════════════════════╪═══════╡
232
+ │ Parameters: ds=0, gs=1 ┆ null │
233
+ │ 1 ┆ 10 │
234
+ │ 2 ┆ 20 │
235
+ └────────────────────────┴───────┘
178
236
  >>> explode_nested_params(df, keys=["ds", "gs"])
179
237
  shape: (2, 4)
180
238
  ┌───────┬───────┬─────┬─────┐
@@ -0,0 +1,337 @@
1
+ """
2
+ Pydantic ready ``quantiphy.Quantity`` units
3
+
4
+ Examples
5
+ --------
6
+
7
+ >>> from pydantic import BaseModel
8
+ >>> class RC(BaseModel):
9
+ ... resistance: Resistance
10
+ ... time: Time
11
+ >>> rc = RC(resistance="4.7 kOhm", time="1n")
12
+ >>> rc.resistance
13
+ Resistance('4.7 kOhm')
14
+ >>> rc.time
15
+ Time('1 ns')
16
+ >>> rc.model_dump_json()
17
+ '{"resistance":4700.0,"time":1e-9}'
18
+ >>> rc.model_dump_json(context={"quantity_format": "str"})
19
+ '{"resistance":"4.7 kOhm","time":"1 ns"}'
20
+ >>> rc.model_dump_json(context={"quantity_format": "as_tuple"})
21
+ '{"resistance":[4700.0,"Ohm"],"time":[1e-9,"s"]}'
22
+
23
+ Notes
24
+ -----
25
+
26
+ The following approaches were considered based on Pydantic docs and
27
+ discussions with a LLM model for parts which confused me.
28
+
29
+ Chosen approach: Put the pydantic hook on the type itself. Works
30
+ everywhere automatically, best autocomplete, but ties Time to pydantic
31
+ and uses a lower-level API pydantic calls experimental.
32
+
33
+ - Annotate as str, secretly swap it to Time in a validator.
34
+ Rejected since: the type hint lie, so
35
+ autocomplete/type-checker/serialization all get confused.
36
+ - model_validator(mode="after"). Only makes sense when one field needs to be derived from
37
+ another (cross-field logic), not for a single field's own conversion.
38
+ - Approach from pydantic_pint seemed unncessary, but might have to consider if
39
+ there are any issues.
40
+ """
41
+
42
+ from typing import TYPE_CHECKING, Any, Self
43
+
44
+ from pydantic import (
45
+ GetCoreSchemaHandler,
46
+ GetJsonSchemaHandler,
47
+ SerializationInfo,
48
+ )
49
+ from pydantic.json_schema import JsonSchemaValue
50
+ from pydantic_core import core_schema
51
+ from quantiphy import (
52
+ QuantiPhyError,
53
+ UnitConversion,
54
+ as_real,
55
+ as_tuple,
56
+ )
57
+ from quantiphy import Quantity as _Quantity
58
+
59
+ UnitConversion(["ohm", "Ohms", "ohms", "Ω"], "Ohm")
60
+
61
+
62
+ class Quantity(_Quantity):
63
+ """
64
+ Wrapper around ``quantiphy.Quantity`` to add Pydantic support.
65
+ """
66
+
67
+ units: str = ""
68
+
69
+ if TYPE_CHECKING:
70
+
71
+ def __new__(
72
+ cls,
73
+ value: float | str | Self,
74
+ *args: Any,
75
+ **kwargs: Any,
76
+ ) -> Self: ...
77
+
78
+ @classmethod
79
+ def __get_pydantic_core_schema__(
80
+ cls, _source_type, _handler: GetCoreSchemaHandler
81
+ ) -> core_schema.CoreSchema:
82
+ """
83
+ Generate a Pydantic core schema for the Quantity type.
84
+
85
+ The schema is union based with the serialization support for
86
+ as_real, str, and as_tuple serialization options using the
87
+ context dictionary.
88
+ """
89
+ return core_schema.union_schema(
90
+ [
91
+ core_schema.is_instance_schema(cls),
92
+ core_schema.no_info_plain_validator_function(cls._parse),
93
+ ],
94
+ mode="left_to_right",
95
+ serialization=core_schema.plain_serializer_function_ser_schema(
96
+ cls._serialize,
97
+ info_arg=True,
98
+ return_schema=core_schema.union_schema(
99
+ [
100
+ core_schema.str_schema(),
101
+ core_schema.float_schema(),
102
+ core_schema.tuple_schema(
103
+ items_schema=[
104
+ core_schema.float_schema(),
105
+ core_schema.str_schema(),
106
+ ],
107
+ strict=True,
108
+ ),
109
+ ]
110
+ ),
111
+ ),
112
+ )
113
+
114
+ @classmethod
115
+ def _parse(cls, value: float | str) -> Self:
116
+ """
117
+ Parse value using Quantiphy's native mechanism.
118
+ """
119
+ try:
120
+ return cls(value)
121
+ except QuantiPhyError as error:
122
+ # TODO: Consider custom error message for user.
123
+ raise ValueError(str(error)) from error
124
+
125
+ @classmethod
126
+ def __get_pydantic_json_schema__(
127
+ cls,
128
+ _core_schema: core_schema.CoreSchema,
129
+ handler: GetJsonSchemaHandler,
130
+ ) -> JsonSchemaValue:
131
+ """Generate a JSON schema for the Quantity type."""
132
+ json_schema = handler(core_schema.str_schema())
133
+ json_schema["description"] = (
134
+ f"Physical quantity with units: {cls.units}"
135
+ if cls.units
136
+ else "Quantiphy quantity without units"
137
+ )
138
+ json_schema["examples"] = (
139
+ [f"1 {cls.units}", f"1 n{cls.units}", f"1 M{cls.units}"]
140
+ if cls.units
141
+ else ["1", "1n", "1M"]
142
+ )
143
+ return json_schema
144
+
145
+ def _serialize(
146
+ self, info: SerializationInfo
147
+ ) -> str | float | tuple[float, str]:
148
+ """Customizable options to serialize the quantity."""
149
+ if isinstance(info.context, dict):
150
+ quantity_format = info.context.get("quantity_format", "as_real")
151
+ match quantity_format:
152
+ case "as_real":
153
+ return as_real(self)
154
+ case "str":
155
+ return str(self)
156
+ case "as_tuple":
157
+ return as_tuple(self)
158
+ case _:
159
+ raise ValueError(
160
+ f"Unknown quantity_format value: {quantity_format}"
161
+ )
162
+ else:
163
+ return as_real(self)
164
+
165
+
166
+ class Time(Quantity):
167
+ """
168
+ Time quantity with units of seconds.
169
+
170
+ Attributes
171
+ ----------
172
+ units : str, default="s"
173
+ Unit symbol.
174
+
175
+ Examples
176
+ --------
177
+ >>> Time("1 ns")
178
+ Time('1 ns')
179
+ >>> Time(1e-9)
180
+ Time('1 ns')
181
+ """
182
+
183
+ units: str = "s"
184
+
185
+
186
+ class Voltage(Quantity):
187
+ """
188
+ Voltage quantity with units of Volts.
189
+
190
+ Attributes
191
+ ----------
192
+ units : str, default="V"
193
+ Unit symbol.
194
+
195
+ Examples
196
+ --------
197
+ >>> Voltage("1 uV")
198
+ Voltage('1 uV')
199
+ >>> Voltage(1e-9)
200
+ Voltage('1 nV')
201
+ """
202
+
203
+ units: str = "V"
204
+
205
+
206
+ class Current(Quantity):
207
+ """
208
+ Current quantity with units of Amperes.
209
+
210
+ Attributes
211
+ ----------
212
+ units : str, default="A"
213
+ Unit symbol.
214
+
215
+ Examples
216
+ --------
217
+ >>> Current("1 uA")
218
+ Current('1 uA')
219
+ >>> Current(1e-9)
220
+ Current('1 nA')
221
+ """
222
+
223
+ units: str = "A"
224
+
225
+
226
+ class Power(Quantity):
227
+ """
228
+ Power quantity with units of Watts.
229
+
230
+ Attributes
231
+ ----------
232
+ units : str, default="W"
233
+ Unit symbol.
234
+
235
+ Examples
236
+ --------
237
+ >>> Power("12 mW")
238
+ Power('12 mW')
239
+ >>> Power(1e3)
240
+ Power('1 kW')
241
+ """
242
+
243
+ units: str = "W"
244
+
245
+
246
+ class Resistance(Quantity):
247
+ """
248
+ Resistance quantity with units of Ohms/ohm/etc.
249
+
250
+ Attributes
251
+ ----------
252
+ units : str, default="Ohm"
253
+ Unit symbol.
254
+
255
+ Examples
256
+ --------
257
+ >>> Resistance("5 ohms")
258
+ Resistance('5 Ohm')
259
+ >>> Resistance("5 mΩ")
260
+ Resistance('5 mOhm')
261
+ >>> Resistance(50)
262
+ Resistance('50 Ohm')
263
+ """
264
+
265
+ units: str = "Ohm"
266
+
267
+
268
+ class Capacitance(Quantity):
269
+ """
270
+ Capacitance quantity with units of Farads.
271
+
272
+ Attributes
273
+ ----------
274
+ units : str, default="F"
275
+ Unit symbol.
276
+
277
+ Examples
278
+ --------
279
+ >>> Capacitance("5e-15 F")
280
+ Capacitance('5 fF')
281
+ """
282
+
283
+ units: str = "F"
284
+
285
+
286
+ class Inductance(Quantity):
287
+ """
288
+ Inductance quantity with units of Henry.
289
+
290
+ Attributes
291
+ ----------
292
+ units : str, default="H"
293
+ Unit symbol.
294
+
295
+ Examples
296
+ --------
297
+ >>> Inductance(5e-9)
298
+ Inductance('5 nH')
299
+ """
300
+
301
+ units: str = "H"
302
+
303
+
304
+ class Temperature(Quantity):
305
+ """
306
+ Temperature quantity with units of Celsius.
307
+
308
+ Attributes
309
+ ----------
310
+ units : str, default="C"
311
+ Unit symbol.
312
+
313
+ Examples
314
+ --------
315
+ >>> Temperature(25)
316
+ Temperature('25 C')
317
+ """
318
+
319
+ units: str = "C"
320
+
321
+
322
+ class Frequency(Quantity):
323
+ """
324
+ Frequency quantity with units of Hertz.
325
+
326
+ Attributes
327
+ ----------
328
+ units : str, default="Hz"
329
+ Unit symbol.
330
+
331
+ Examples
332
+ --------
333
+ >>> Frequency(5e9)
334
+ Frequency('5 GHz')
335
+ """
336
+
337
+ units: str = "Hz"
@@ -1 +0,0 @@
1
- """Top-level package for Analog Daddy."""