|
1 | | -# Funkypy |
| 1 | +# LuxTools |
| 2 | +> A collection of tools and utilities that I often use in my work. |
2 | 3 |
|
3 | | -A collection of functional programming utilities for Python. |
| 4 | +## Features |
4 | 5 |
|
| 6 | +- **[Scientific](#scientific)** |
| 7 | + - [Error propagation](#error-propagation) for numerical calculations |
| 8 | + - [Pretty printing](#numeric-result-formatting) of numerical results with uncertainties |
| 9 | +- **[Functional](#functional)** |
| 10 | + - [Function composition](#function-composition) utilities |
| 11 | + - [Partial function](#partial-application) application |
| 12 | + - [Overload function](#overload-function-definitions) definitions |
5 | 13 |
|
| 14 | +## Installation |
| 15 | + |
| 16 | +```bash |
| 17 | +pip install luxtools |
6 | 18 | ``` |
7 | | -partial |
8 | | -``` |
9 | 19 |
|
10 | | -Okay so i think I understand the problem of multiple partial: |
11 | | -- @partial return a function, and it's that function we edit to apply it. |
12 | | -- It should return the function we get to return the function that can the be partialled every time we try again. |
13 | | - - maybe upon successful call |
14 | | - - or maybe should give up and use functools.partial. |
| 20 | +## Usage |
| 21 | + |
| 22 | +### Scientific |
15 | 23 |
|
| 24 | +#### Error Propagation |
16 | 25 |
|
| 26 | +```python |
| 27 | +import torch |
| 28 | +from luxtools import get_error |
17 | 29 |
|
18 | | -# Significant Figures |
| 30 | +# Create tensors with uncertainties |
| 31 | +x = torch.tensor([1.0, 3.0], dtype=torch.float32, requires_grad=True) |
| 32 | +x.sigma = torch.tensor([0.1, 0.2], dtype=torch.float32) |
19 | 33 |
|
20 | | -## Installation |
21 | | -Navigate to the root directory of the repository and run the following command: |
| 34 | +y = torch.tensor([2.0, 4.0], dtype=torch.float32, requires_grad=True) |
| 35 | +y = Variable(y, torch.tensor([0.2, 0.3])) |
22 | 36 |
|
23 | | -```bash |
24 | | -pip install -e . |
| 37 | +# Perform calculations |
| 38 | +f = x * y |
| 39 | + |
| 40 | +# Get propagated error |
| 41 | +error = get_error(f) |
| 42 | +# tensor([0.2828, 1.2042]) |
25 | 43 | ``` |
26 | 44 |
|
27 | | -## Usage |
| 45 | +#### Numeric Result Formatting |
| 46 | + |
28 | 47 | ```python |
29 | | -from significant_figures import NumericResult |
| 48 | +from luxtools import NumericResult |
30 | 49 |
|
31 | | -result = NumericResult(value=1.2345, uncertainty=0.01, unit='m') |
| 50 | +# Create a measurement with uncertainty |
| 51 | +result = NumericResult(1.234, 0.193) |
| 52 | +print(result) |
| 53 | +# (1.2 ± 0.2) |
32 | 54 |
|
| 55 | +# Scientific notation |
| 56 | +result = NumericResult(234.23424, 10) |
33 | 57 | print(result) |
34 | | -# Output: (1.23 ± 0.01) m |
| 58 | +# (2.3 ± 0.1)*10^(2) |
| 59 | + |
| 60 | +# LaTeX output |
| 61 | +print(result.latex()) |
| 62 | +# (2.3 \pm 0.1)\cdot 10^{2} |
| 63 | +``` |
| 64 | + |
| 65 | +### Functional |
| 66 | + |
| 67 | +#### Function Composition |
| 68 | + |
| 69 | +```python |
| 70 | +from luxtools import chain |
| 71 | + |
| 72 | +# Compose functions |
| 73 | +f = lambda x: x + 1 |
| 74 | +g = lambda x: x * 2 |
| 75 | +h = lambda x: x ** 2 |
| 76 | + |
| 77 | +# Create a new function that applies f, then g, then h |
| 78 | +composed = chain(f, g, h) |
35 | 79 |
|
36 | | -print(result.latex(delimiter="")) |
37 | | -# Output: (1.23 \\pm 0.01) m |
| 80 | +result = composed(3) # ((3 + 1) * 2) ** 2 = 64 |
38 | 81 | ``` |
39 | 82 |
|
| 83 | +#### Partial Application |
| 84 | +Allows you to partially apply arguments to a function, creating a new function with fewer arguments. See [article](https://lunalux.io/functional-programming-in-python/better-currying-in-python/) for discussion. |
| 85 | + |
| 86 | +```python |
| 87 | +from luxtools import partial |
| 88 | + |
| 89 | +@partial |
| 90 | +def greet(greeting, name): |
| 91 | + return f"{greeting}, {name}!" |
| 92 | + |
| 93 | +# Create a new function with 'Hello' fixed as the greeting |
| 94 | +say_hello = greet("Hello") |
| 95 | + |
| 96 | +result = say_hello("World") # "Hello, World!" |
| 97 | +``` |
| 98 | + |
| 99 | +#### Overload function definitions |
| 100 | + |
| 101 | +Allows you to have multiple function definitions for the same function name. |
| 102 | +It uses typehints to determine which function to call. See [article](https://lunalux.io/functional-programming-in-python/overloading-functions-in-python/) for discussion. |
| 103 | + |
| 104 | +```python |
| 105 | +from luxtools import overload |
| 106 | + |
| 107 | +class Email: |
| 108 | + def __init__(self, email: str): |
| 109 | + self.email = email |
| 110 | + |
| 111 | + def __str__(self) -> str: |
| 112 | + return self.email |
| 113 | + |
| 114 | + |
| 115 | +class PhoneNumber: |
| 116 | + def __init__(self, phone_number: str): |
| 117 | + self.phone_number = phone_number |
| 118 | + |
| 119 | + def __str__(self) -> str: |
| 120 | + return self.phone_number |
| 121 | + |
| 122 | + |
| 123 | +@overload |
| 124 | +def get_user(email: Email): |
| 125 | + print("Email:", email) |
| 126 | + return email |
| 127 | + |
| 128 | + |
| 129 | +@overload |
| 130 | +def get_user(phone_number: PhoneNumber): |
| 131 | + print("Phone:", phone_number) |
| 132 | + return phone_number |
| 133 | + |
| 134 | +get_user(Email("test@example.com")) # prints: Email: test@example.com |
| 135 | +get_user(PhoneNumber("123-456-789")) # prints: Phone: 123-456-789 |
| 136 | +``` |
| 137 | + |
| 138 | +Caveat, if the function is defined in a non-global scope such as a class, or inside a function, then you need to pass the local scope to the decorator. |
| 139 | + |
| 140 | +```python |
| 141 | +def local_scope(): |
| 142 | + @overload(scope=locals()) |
| 143 | + def get_user(email: Email): |
| 144 | + print("Email:", email) |
| 145 | + |
| 146 | + @overload(scope=locals()) |
| 147 | + def get_user(phone_number: PhoneNumber): |
| 148 | + print("Phone:", phone_number) |
| 149 | +``` |
40 | 150 |
|
41 | | -## Examples |
42 | | -```Python |
43 | | -Value, Uncertainty, expected result. |
44 | | -(1e-4, 1e-5, "(1.0 +/- 0.1)*10^(-4)"), |
45 | | -(1, 0.1, "(1.0 +/- 0.1)"), |
46 | | -(1.234, 0.193, "(1.2 +/- 0.2)"), |
47 | | -(0.1, 1, "(0 +/- 1)"), |
48 | | -(234.23424,10, "(2.3 +/- 0.1)*10^(2)"), |
49 | | -(0, 0, "(0 +/- 0)"), |
50 | | -(10, 0, "(1.0 +/- 0.0)*10^(1)"), |
51 | | -(0, 10, "(0 +/- 1)*10^(1)"), |
52 | | -(0.123456789, 0.987654321, "(1 +/- 10)*10^(-1)"), |
53 | | -(123456789, 987654321, "(1 +/- 10)*10^(8)"), |
54 | | -(1.23456789e-9, 9.87654321e-11, "(1.23 +/- 0.10)*10^(-9)"), |
55 | | -``` |
| 151 | +This is necessary because the parent stack frame doesn't exist inside the `overload` function, so it has to be passed explicitly. See [tests](test/functional/test_overload.py). |
0 commit comments