physpyx 3.0.0__tar.gz → 3.2.0__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: physpyx
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: Provides a way to use python code in LaTeX
5
5
  Author: Jérôme Dufour
6
6
  Author-email: Jérôme Dufour <jerome.dufour@eduvaud.ch>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "physpyx"
3
- version = "3.0.0"
3
+ version = "3.2.0"
4
4
  description = "Provides a way to use python code in LaTeX"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "physpyx"
3
- version = "3.0.0"
3
+ version = "3.2.0"
4
4
  description = "Provides a way to use python code in LaTeX"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -40,6 +40,11 @@ class Orbit:
40
40
  else:
41
41
  raise ValueError
42
42
 
43
+ if self.a < self.b:
44
+ raise ValueError(
45
+ "The semi-major axis should be bigger or equal to the semi-minor axis (a>=b)"
46
+ )
47
+
43
48
  self.c = self.a * self.e # linear eccentricity
44
49
  self.p = self.a * (1.0 - self.e * self.e) # semi-latus
45
50
  self.T = 2 * np.pi * self.a * np.sqrt(self.a / self.mu) # period
@@ -69,11 +74,7 @@ class Orbit:
69
74
  r = self.p / (1 + self.e * np.cos(self.theta))
70
75
  self.x = r * np.array([np.cos(self.theta), np.sin(self.theta)]) + self.v * dt
71
76
 
72
- def run(
73
- self,
74
- steps: int,
75
- every: int = 1,
76
- ) -> Iterator[NDArray]:
77
+ def run(self, steps: int, every: int = 1) -> Iterator[NDArray]:
77
78
  if steps % every:
78
79
  raise ValueError(f"{every} is not a divisor {steps} steps")
79
80
 
@@ -1,7 +1,10 @@
1
1
  import re
2
2
  import sys
3
+ from collections.abc import Iterable
3
4
  from logging import getLogger
5
+ from typing import cast
4
6
 
7
+ from numpy import array
5
8
  from pint._typing import Scalar
6
9
  from pint.facets.context import Context
7
10
 
@@ -56,7 +59,7 @@ class Qty(PTManager):
56
59
 
57
60
  def __call__(
58
61
  self,
59
- value: Scalar | list[Scalar] | str,
62
+ value: Scalar | Iterable[Scalar | SiunitxQuantity] | str,
60
63
  unit: str | None = None,
61
64
  to: str | None = None,
62
65
  fmt: str = "g",
@@ -90,6 +93,22 @@ class Qty(PTManager):
90
93
  if unit is not None:
91
94
  logger.warning("Units are ignored when instanciating a constant")
92
95
  value, unit, fmt = self._constants(value, fmt)
96
+ elif isinstance(value, Iterable):
97
+ if all(isinstance(item, SiunitxQuantity) for item in value):
98
+ value = cast(Iterable[SiunitxQuantity], value)
99
+ dim = {v.dimensionality for v in value}
100
+ if len(dim) != 1:
101
+ raise ValueError(
102
+ f"Can't create a vector with different dimensionality ({dim})"
103
+ )
104
+
105
+ if unit == None:
106
+ unit = ""
107
+ if self._ureg.Unit(unit).dimensionality != dim.pop():
108
+ raise ValueError("Need to set unit with correct dimensionality")
109
+ value = array([v.fmtex(to=unit).m for v in value])
110
+ else:
111
+ value = array(value)
93
112
 
94
113
  q = self._ureg.Quantity(
95
114
  value,
@@ -1,8 +1,8 @@
1
- from collections.abc import Generator, Iterator
1
+ from collections.abc import Generator, Iterable, Iterator
2
2
  from logging import getLogger
3
3
  from typing import ClassVar, Self, TypedDict, cast
4
4
 
5
- from numpy import cross, linalg, ndarray
5
+ from numpy import cross, linalg
6
6
  from pint._typing import Magnitude, UnitLike
7
7
  from pint.registry import Quantity
8
8
 
@@ -82,8 +82,8 @@ class SiunitxQuantity(Quantity):
82
82
  return alt
83
83
 
84
84
  def __iter__(self) -> Iterator["SiunitxQuantity"]:
85
- # NOTE: this is used to set formatting options in tuple expansions:
86
- # rx, ry = qty([3, 2], "kilometer", fmt=".1e")
85
+ # NOTE: this is used to set formatting options in tuple expansions rx,
86
+ # ry = qty([3, 2], "kilometer", fmt=".1e")
87
87
  for val in super().__iter__():
88
88
  val._fmt = self._fmt
89
89
  val._base = self._base
@@ -99,33 +99,35 @@ class SiunitxQuantity(Quantity):
99
99
  elif self._preferred:
100
100
  quantity = self._REGISTRY.to_preferred_units(self)
101
101
  if self._compact:
102
+ if isinstance(self.m, Iterable):
103
+ raise ValueError("'compact=True' for vectors has an undefined behavior")
102
104
  quantity = cast(SiunitxQuantity, self.to_compact())
103
105
 
104
106
  quantity._fmt = self._fmt
105
107
  return (
106
108
  quantity._format_array()
107
- if isinstance(quantity.m, ndarray)
109
+ if isinstance(quantity.m, Iterable)
108
110
  else quantity._format_scalar()
109
111
  )
110
112
 
113
+ def _format_magnitude(self, m: float) -> str:
114
+ out = f"{m:{self._fmt}}"
115
+ if out.startswith("1e"):
116
+ return out[1:]
117
+ elif out.startswith("-1e"):
118
+ return "-" + out[2:]
119
+ return out
120
+
111
121
  def _format_array(self) -> str:
112
122
  if self.m.ndim != 1:
113
123
  raise NotImplementedError
114
124
  out = r"\begin{pmatrix}"
115
125
  if isinstance(self.m[0], complex):
116
126
  out += "\\\\".join(
117
- [
118
- rf"\complexnum{{{v:{self._fmt}}}}" if abs(v) > 1e-16 else r"\num{0}"
119
- for v in self.m
120
- ]
127
+ rf"\complexnum{{{self._format_magnitude(m)}}}" for m in self.m
121
128
  )
122
129
  else:
123
- out += "\\\\".join(
124
- [
125
- rf"\num{{{v:{self._fmt}}}}" if abs(v) > 1e-16 else r"\num{0}"
126
- for v in self.m
127
- ]
128
- )
130
+ out += "\\\\".join(rf"\num{{{self._format_magnitude(m)}}}" for m in self.m)
129
131
 
130
132
  out += r"\end{pmatrix}"
131
133
  if ustr := self._format_unit():
@@ -133,9 +135,7 @@ class SiunitxQuantity(Quantity):
133
135
  return out
134
136
 
135
137
  def _format_scalar(self) -> str:
136
- out = f"{self.m:{self._fmt}}"
137
- if out.startswith("1e"):
138
- out = out[1:]
138
+ out = self._format_magnitude(self.m)
139
139
  if ustr := self._format_unit():
140
140
  if isinstance(self.m, complex):
141
141
  return rf"\complexqty{{{out}}}{{{ustr}}}"
@@ -157,6 +157,21 @@ class SiunitxQuantity(Quantity):
157
157
  # "PlainQuantity[Unknown]"'
158
158
  return cast(SiunitxQuantity, super().__add__(other))
159
159
 
160
+ def __truediv__(self, other: object) -> "SiunitxQuantity":
161
+ # NOTE: to fix typing warning 'Cannot access attribute "???" for class
162
+ # "PlainQuantity[Unknown]"'
163
+ return cast(SiunitxQuantity, super().__truediv__(other))
164
+
165
+ def __mul__(self, other: object) -> "SiunitxQuantity":
166
+ # NOTE: to fix typing warning 'Cannot access attribute "???" for class
167
+ # "PlainQuantity[Unknown]"'
168
+ return cast(SiunitxQuantity, super().__mul__(other))
169
+
170
+ def __rmul__(self, other: object) -> "SiunitxQuantity":
171
+ # NOTE: to fix typing warning 'Cannot access attribute "???" for class
172
+ # "PlainQuantity[Unknown]"'
173
+ return cast(SiunitxQuantity, super().__rmul__(other))
174
+
160
175
  def _format_unit(self) -> str:
161
176
  num = ""
162
177
  den = ""
@@ -0,0 +1,196 @@
1
+ from collections.abc import Generator
2
+ from enum import IntEnum
3
+ from typing import Self
4
+
5
+ import numpy as np
6
+
7
+
8
+ class Coordinate:
9
+ def __init__(self, x: float, y: float):
10
+ self.x = x
11
+ self.y = y
12
+
13
+ def norm(self) -> float:
14
+ return np.sqrt(self.x * self.x + self.y * self.y)
15
+
16
+ def orientation(self) -> float:
17
+ return np.atan2(self.y, self.x) * 180 / np.pi
18
+
19
+ def __isub__(self, other: "Coordinate") -> Self:
20
+ self.x -= other.x
21
+ self.y -= other.y
22
+ return self
23
+
24
+ def __sub__(self, other: "Coordinate") -> "Coordinate":
25
+ return Coordinate(self.x - other.x, self.y - other.y)
26
+
27
+ def __iadd__(self, other: "Coordinate") -> Self:
28
+ self.x += other.x
29
+ self.y += other.y
30
+ return self
31
+
32
+ def __mul__(self, number: float) -> "Coordinate":
33
+ return Coordinate(self.x * number, self.y * number)
34
+
35
+ __rmul__ = __mul__
36
+
37
+ def __truediv__(self, number: float) -> "Coordinate":
38
+ return Coordinate(self.x / number, self.y / number)
39
+
40
+ def __add__(self, other: "Coordinate") -> "Coordinate":
41
+ return Coordinate(self.x + other.x, self.y + other.y)
42
+
43
+ def dot(self, other: "Coordinate") -> float:
44
+ return self.x * other.x + self.y * other.y
45
+
46
+ def __str__(self) -> str:
47
+ return f"({self.x}, {self.y})"
48
+
49
+
50
+ class Rectangle:
51
+ def __init__(self, c1: Coordinate, c2: Coordinate):
52
+ self.c1 = c1
53
+ self.c2 = c2
54
+
55
+ def __str__(self) -> str:
56
+ return f"{self.c1} rectangle {self.c2}"
57
+
58
+ @property
59
+ def center(self) -> Coordinate:
60
+ return 0.5 * self.c1 + 0.5 * self.c2
61
+
62
+ @property
63
+ def north(self) -> Coordinate:
64
+ return Coordinate(0.5 * self.c1.x + 0.5 * self.c2.x, max(self.c1.y, self.c2.y))
65
+
66
+ @property
67
+ def south(self) -> Coordinate:
68
+ return Coordinate(0.5 * self.c1.x + 0.5 * self.c2.x, min(self.c1.y, self.c2.y))
69
+
70
+ @property
71
+ def east(self) -> Coordinate:
72
+ return Coordinate(max(self.c1.x, self.c2.x), 0.5 * self.c1.y + 0.5 * self.c2.y)
73
+
74
+ @property
75
+ def west(self) -> Coordinate:
76
+ return Coordinate(min(self.c1.x, self.c2.x), 0.5 * self.c1.y + 0.5 * self.c2.y)
77
+
78
+ @property
79
+ def north_east(self) -> Coordinate:
80
+ return Coordinate(max(self.c1.x, self.c2.x), max(self.c1.y, self.c2.y))
81
+
82
+ @property
83
+ def north_west(self) -> Coordinate:
84
+ return Coordinate(min(self.c1.x, self.c2.x), max(self.c1.y, self.c2.y))
85
+
86
+ @property
87
+ def south_west(self) -> Coordinate:
88
+ return Coordinate(min(self.c1.x, self.c2.x), min(self.c1.y, self.c2.y))
89
+
90
+ @property
91
+ def south_east(self) -> Coordinate:
92
+ return Coordinate(max(self.c1.x, self.c2.x), min(self.c1.y, self.c2.y))
93
+
94
+
95
+ class Circle:
96
+ class LR(IntEnum):
97
+ LEFT = -1
98
+ RIGHT = 1
99
+
100
+ class IO(IntEnum):
101
+ OUTER = -1
102
+ INNER = 1
103
+
104
+ class ORIENT(IntEnum):
105
+ DEFAULT = 0
106
+ CLOCKWISE = -1
107
+ COUNTER_CLOCKWISE = 1
108
+
109
+ def __init__(self, center: Coordinate, radius: float):
110
+ self.center = center
111
+ self.radius = radius
112
+
113
+ def __str__(self) -> str:
114
+ return f"{self.center} circle ({self.radius})"
115
+
116
+ def tangent_points_to_circle(
117
+ self, circ: "Circle", lr: LR, io: IO
118
+ ) -> tuple[Coordinate, Coordinate]:
119
+ D = circ.center - self.center
120
+ d = D.norm()
121
+ R = (circ.radius + io * self.radius) / d
122
+ e1 = D / d
123
+ e2 = Coordinate(-e1.y, e1.x)
124
+
125
+ ab = R * e1 + lr * np.sqrt(1 - R * R) * e2
126
+ c = -io * self.radius - ab.dot(self.center)
127
+ return (
128
+ self.center - ab * (ab.dot(self.center) + c),
129
+ circ.center - ab * (ab.dot(circ.center) + c),
130
+ )
131
+
132
+ def tangent_points_passing_by(self, P: Coordinate, lr: LR) -> Coordinate:
133
+ D = P - self.center
134
+ d = D.norm()
135
+ e1 = D / d
136
+ e2 = Coordinate(-e1.y, e1.x)
137
+ a = self.radius * self.radius / d * e1
138
+ b = self.radius / d * np.sqrt(d * d - self.radius * self.radius) * e2
139
+ return self.center + a + lr * b
140
+
141
+
142
+ class Pulley(Circle):
143
+ def to_tex(self) -> Generator[str]:
144
+ yield rf"\fill[black!30] {self};"
145
+ yield rf"\draw[black!50, line width={2 * self.radius}] {Circle(self.center, 0.85 * self.radius)};"
146
+ yield rf"\fill[black!70] {Circle(self.center, 0.25 * self.radius)};"
147
+ shift = np.random.random() * np.pi / 3
148
+ for i in range(6):
149
+ c = self.center + Coordinate(
150
+ 0.6 * self.radius * np.sin(i * np.pi / 3 + shift),
151
+ 0.6 * self.radius * np.cos(i * np.pi / 3 + shift),
152
+ )
153
+ yield rf"\fill[black!60]{Circle(c, 0.1 * self.radius)};"
154
+
155
+
156
+ class Rope:
157
+ def __init__(self, start: Coordinate):
158
+ self.current = start
159
+ self._path = f"{start}"
160
+
161
+ def __str__(self) -> str:
162
+ return self._path
163
+
164
+ def to_pulley(self, pulley: Pulley, lr: Circle.LR) -> None:
165
+ self.current = pulley.tangent_points_passing_by(self.current, lr)
166
+ self._path += f" -- {self.current}"
167
+
168
+ def _compute_angles(
169
+ self, a: Coordinate, b: Coordinate, o: Circle.ORIENT
170
+ ) -> tuple[float, float]:
171
+ return (self.current - a).orientation(), (b - a).orientation() + o * 360
172
+
173
+ def from_pulley(
174
+ self,
175
+ pulley: Pulley,
176
+ to: Coordinate,
177
+ lr: Circle.LR,
178
+ orientation: Circle.ORIENT = Circle.ORIENT.DEFAULT,
179
+ ) -> None:
180
+ tmp = pulley.tangent_points_passing_by(to, lr)
181
+ angle_i, angle_o = self._compute_angles(pulley.center, tmp, orientation)
182
+ self._path += f" arc ({angle_i}:{angle_o}:{pulley.radius}) -- {to}"
183
+ self.current = to
184
+
185
+ def from_pulley_to_pulley(
186
+ self,
187
+ P1: Pulley,
188
+ P2: Pulley,
189
+ lr: Circle.LR,
190
+ io: Circle.IO,
191
+ orientation: Circle.ORIENT = Circle.ORIENT.DEFAULT,
192
+ ) -> None:
193
+ t1, t2 = P1.tangent_points_to_circle(P2, lr, io)
194
+ angle_i, angle_o = self._compute_angles(P1.center, t1, orientation)
195
+ self._path += f" arc ({angle_i}:{angle_o}:{P1.radius}) -- {t2}"
196
+ self.current = t2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes