complex-range 1.0.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.
@@ -0,0 +1,25 @@
1
+ """
2
+ complex-range - Generate ranges of complex numbers.
3
+
4
+ A Python implementation of the Wolfram Language ComplexRange resource function,
5
+ originally created by Daniele Gregori.
6
+
7
+ Generates rectangular and linear ranges of complex numbers in the complex plane.
8
+
9
+ Example usage:
10
+ >>> from complex_range import complex_range
11
+ >>> list(complex_range(0, 2+2j))
12
+ [0j, 1j, 2j, (1+0j), (1+1j), (1+2j), (2+0j), (2+1j), (2+2j)]
13
+
14
+ >>> list(complex_range([0, 1+1j])) # Linear range
15
+ [0j, (1+1j)]
16
+
17
+ Author: Daniele Gregori
18
+ """
19
+
20
+ from .core import complex_range, complex_range_iter, ComplexRangeError
21
+ from .farey import farey_sequence
22
+
23
+ __version__ = "1.0.0"
24
+ __author__ = "Daniele Gregori"
25
+ __all__ = ["complex_range", "complex_range_iter", "ComplexRangeError", "farey_sequence"]
complex_range/core.py ADDED
@@ -0,0 +1,549 @@
1
+ """
2
+ Core implementation of the ComplexRange function.
3
+
4
+ This module provides the complex_range function which generates ranges of
5
+ complex numbers in either rectangular (grid) or linear (diagonal) patterns.
6
+
7
+ Author: Daniele Gregori
8
+ """
9
+
10
+ from fractions import Fraction
11
+ from typing import List, Tuple, Union, Iterator, Optional, Literal
12
+ from numbers import Number
13
+
14
+ from .farey import farey_sequence
15
+
16
+
17
+ class ComplexRangeError(Exception):
18
+ """Exception raised for ComplexRange-specific errors."""
19
+ pass
20
+
21
+
22
+ def _farey_range_values(start: float, end: float, order: int) -> List[Union[int, float, Fraction]]:
23
+ """
24
+ Generate values for a Farey range.
25
+
26
+ The Farey sequence of the given order is applied to each unit interval
27
+ in the range, and all points are combined.
28
+
29
+ Parameters
30
+ ----------
31
+ start : float
32
+ Start of the range.
33
+ end : float
34
+ End of the range.
35
+ order : int
36
+ Order of the Farey sequence to use.
37
+
38
+ Returns
39
+ -------
40
+ List
41
+ Sorted list of values in the range.
42
+ """
43
+ if order < 1:
44
+ raise ComplexRangeError(f"Farey order must be positive, got {order}")
45
+
46
+ # Get the Farey sequence for this order
47
+ farey = farey_sequence(order)
48
+
49
+ # Determine the number of unit intervals
50
+ span = end - start
51
+ num_units = int(span)
52
+
53
+ # If span is not an integer, we need to handle fractional part
54
+ if span != num_units:
55
+ # For non-integer spans, just scale the Farey sequence
56
+ result = sorted(set(float(start + f * span) for f in farey))
57
+ return result
58
+
59
+ # For integer spans, apply Farey to each unit interval
60
+ all_values = set()
61
+
62
+ for unit in range(num_units):
63
+ unit_start = start + unit
64
+ for f in farey:
65
+ all_values.add(unit_start + float(f))
66
+
67
+ # Convert to sorted list
68
+ result = sorted(all_values)
69
+
70
+ # Try to keep as fractions if possible
71
+ if all(isinstance(x, int) or (isinstance(x, float) and x == int(x)) for x in [start, end]):
72
+ frac_result = []
73
+ for val in result:
74
+ try:
75
+ frac = Fraction(val).limit_denominator(1000)
76
+ if abs(float(frac) - val) < 1e-10:
77
+ frac_result.append(frac)
78
+ else:
79
+ frac_result.append(val)
80
+ except (ValueError, OverflowError):
81
+ frac_result.append(val)
82
+ return frac_result
83
+
84
+ return result
85
+
86
+
87
+ def _to_number(z: Union[complex, float, int, Fraction]) -> Union[complex, Fraction, int, float]:
88
+ """Convert input to appropriate numeric type, preserving exactness where possible."""
89
+ if isinstance(z, (int, Fraction)):
90
+ return z
91
+ if isinstance(z, float):
92
+ # Try to convert to Fraction if it's a "nice" number
93
+ try:
94
+ frac = Fraction(z).limit_denominator(10000)
95
+ if abs(float(frac) - z) < 1e-10:
96
+ return frac
97
+ except (ValueError, OverflowError):
98
+ pass
99
+ return z
100
+ if isinstance(z, complex):
101
+ return z
102
+ return z
103
+
104
+
105
+ def _arange_inclusive(start: Union[int, float, Fraction],
106
+ end: Union[int, float, Fraction],
107
+ step: Union[int, float, Fraction]) -> List[Union[int, float, Fraction]]:
108
+ """
109
+ Generate a range from start to end (inclusive) with given step.
110
+
111
+ Similar to numpy.arange but includes the endpoint if it's exactly reachable.
112
+ Handles both positive and negative steps.
113
+ """
114
+ if step == 0:
115
+ raise ComplexRangeError("Step cannot be zero")
116
+
117
+ # Handle mismatched direction (positive step but going down, or negative step but going up)
118
+ if (step > 0 and start > end) or (step < 0 and start < end):
119
+ return []
120
+
121
+ result = []
122
+ current = start
123
+
124
+ # Use Fraction for exact arithmetic when possible
125
+ if isinstance(start, (int, Fraction)) and isinstance(end, (int, Fraction)) and isinstance(step, (int, Fraction)):
126
+ start = Fraction(start)
127
+ end = Fraction(end)
128
+ step = Fraction(step)
129
+ current = start
130
+
131
+ if step > 0:
132
+ while current <= end:
133
+ result.append(current)
134
+ current = current + step
135
+ else:
136
+ # Negative step: go downward
137
+ while current >= end:
138
+ result.append(current)
139
+ current = current + step
140
+ else:
141
+ # Floating point version with tolerance
142
+ eps = abs(step) * 1e-14
143
+ if step > 0:
144
+ while current <= end + eps:
145
+ result.append(current)
146
+ current = current + step
147
+ else:
148
+ # Negative step: go downward
149
+ while current >= end - eps:
150
+ result.append(current)
151
+ current = current + step
152
+
153
+ return result
154
+
155
+
156
+ def _rectangular_range(
157
+ z1: complex,
158
+ z2: complex,
159
+ step: Union[complex, Tuple[Number, Number], None] = None,
160
+ increment_first: Literal['im', 're'] = 'im',
161
+ farey_range: bool = False
162
+ ) -> List[complex]:
163
+ """
164
+ Generate a rectangular grid of complex numbers.
165
+
166
+ Parameters
167
+ ----------
168
+ z1 : complex
169
+ First corner of the rectangle (minimum real and imaginary parts).
170
+ z2 : complex
171
+ Second corner of the rectangle (maximum real and imaginary parts).
172
+ step : complex or tuple, optional
173
+ Step size. Can be a complex number (real part = real step, imag part = imag step)
174
+ or a tuple (real_step, imag_step). Default is 1+1j.
175
+ increment_first : {'im', 're'}
176
+ Which component to increment first in the iteration. Default is 'im'.
177
+ farey_range : bool
178
+ If True, use Farey sequence to generate intermediate points.
179
+
180
+ Returns
181
+ -------
182
+ List[complex]
183
+ List of complex numbers forming a rectangular grid.
184
+ """
185
+
186
+ # Extract real and imaginary bounds
187
+ re_min = z1.real
188
+ re_max = z2.real
189
+ im_min = z1.imag
190
+ im_max = z2.imag
191
+
192
+ # Determine step sizes
193
+ if step is None:
194
+ re_step = 1
195
+ im_step = 1
196
+ elif isinstance(step, (list, tuple)):
197
+ re_step, im_step = step
198
+ else:
199
+ # Complex number: real part is real step, imag part is imag step
200
+ re_step = step.real if hasattr(step, 'real') else step
201
+ im_step = step.imag if hasattr(step, 'imag') else step
202
+
203
+ # Check for reversed range - return empty in case
204
+ if ((z1.real > z2.real and (re_step > 0)) and (z1.imag > z2.imag and (im_step > 0))): ###
205
+ return []
206
+ elif ((z1.real < z2.real and (re_step < 0)) and (z1.imag < z2.imag and (im_step < 0))): ###
207
+ return []
208
+
209
+
210
+ # Handle Farey range
211
+ if farey_range:
212
+ # Farey range requires integer step
213
+ if not (isinstance(re_step, int) or (isinstance(re_step, float) and re_step.is_integer())):
214
+ raise ComplexRangeError(
215
+ f"No Farey range with non-integer third argument {re_step} is allowed."
216
+ )
217
+ if not (isinstance(im_step, int) or (isinstance(im_step, float) and im_step.is_integer())):
218
+ raise ComplexRangeError(
219
+ f"No Farey range with non-integer third argument {im_step} is allowed."
220
+ )
221
+
222
+ re_order = int(re_step)
223
+ im_order = int(im_step)
224
+
225
+ # Generate Farey-based ranges
226
+ # The Farey sequence is applied to each unit interval, then combined
227
+ re_values = _farey_range_values(re_min, re_max, re_order)
228
+ im_values = _farey_range_values(im_min, im_max, im_order)
229
+ else:
230
+ # Regular range
231
+ re_values = _arange_inclusive(re_min, re_max, re_step)
232
+ im_values = _arange_inclusive(im_min, im_max, im_step)
233
+
234
+ # Handle empty ranges
235
+ if not re_values or not im_values:
236
+ return []
237
+
238
+ # Generate the grid
239
+ result = []
240
+ if increment_first == 'im':
241
+ # Increment imaginary first (default): for each real, iterate through imaginaries
242
+ for re in re_values:
243
+ for im in im_values:
244
+ result.append(_make_complex(re, im))
245
+ else:
246
+ # Increment real first: for each imaginary, iterate through reals
247
+ for im in im_values:
248
+ for re in re_values:
249
+ result.append(_make_complex(re, im))
250
+
251
+ return result
252
+
253
+
254
+ def _make_complex(re: Union[int, float, Fraction], im: Union[int, float, Fraction]) -> complex:
255
+ """Create a complex number from real and imaginary parts."""
256
+ # Convert Fractions to float for complex number creation
257
+ re_val = float(re) if isinstance(re, Fraction) else re
258
+ im_val = float(im) if isinstance(im, Fraction) else im
259
+
260
+ # Simplify representation
261
+ if im_val == 0:
262
+ if isinstance(re_val, float) and re_val.is_integer():
263
+ return complex(int(re_val), 0)
264
+ return complex(re_val, 0)
265
+ if re_val == 0:
266
+ if isinstance(im_val, float) and im_val.is_integer():
267
+ return complex(0, int(im_val))
268
+ return complex(0, im_val)
269
+
270
+ return complex(re_val, im_val)
271
+
272
+
273
+ def _linear_range(
274
+ z1: complex,
275
+ z2: complex,
276
+ step: Optional[Union[complex, Tuple[Number, Number]]] = None,
277
+ farey_range: bool = False
278
+ ) -> List[complex]:
279
+ """
280
+ Generate a linear (diagonal) range of complex numbers.
281
+
282
+ Points lie on a line from z1 to z2, with both real and imaginary
283
+ components incrementing together.
284
+
285
+ Parameters
286
+ ----------
287
+ z1 : complex
288
+ Starting point.
289
+ z2 : complex
290
+ Ending point.
291
+ step : complex or tuple, optional
292
+ Step sizes. Can be a complex number (real part = real step, imag part = imag step)
293
+ or a tuple (real_step, imag_step). Default is (1, 1).
294
+ farey_range : bool
295
+ If True, use Farey sequence to generate intermediate points.
296
+
297
+ Returns
298
+ -------
299
+ List[complex]
300
+ List of complex numbers along the line from z1 to z2.
301
+ """
302
+ if step is None:
303
+ re_step = 1
304
+ im_step = 1
305
+ elif isinstance(step, (list, tuple)):
306
+ re_step, im_step = step
307
+ elif isinstance(step, complex):
308
+ re_step = step.real
309
+ im_step = step.imag
310
+ else:
311
+ # Single number: use as both steps
312
+ re_step = step
313
+ im_step = step
314
+
315
+ # Get component ranges
316
+ re_start, re_end = z1.real, z2.real
317
+ im_start, im_end = z1.imag, z2.imag
318
+
319
+ # Handle Farey range
320
+ if farey_range:
321
+ # Farey range requires integer step
322
+ if not (isinstance(re_step, int) or (isinstance(re_step, float) and re_step.is_integer())):
323
+ raise ComplexRangeError(
324
+ f"No Farey range with non-integer third argument {re_step} is allowed."
325
+ )
326
+ if not (isinstance(im_step, int) or (isinstance(im_step, float) and im_step.is_integer())):
327
+ raise ComplexRangeError(
328
+ f"No Farey range with non-integer third argument {im_step} is allowed."
329
+ )
330
+
331
+ re_order = int(re_step)
332
+ im_order = int(im_step)
333
+
334
+ # Generate Farey-based ranges
335
+ re_values = _farey_range_values(re_start, re_end, re_order)
336
+ im_values = _farey_range_values(im_start, im_end, im_order)
337
+ else:
338
+ # Generate ranges for each component
339
+ re_values = _arange_inclusive(re_start, re_end, re_step)
340
+ im_values = _arange_inclusive(im_start, im_end, im_step)
341
+
342
+ # For linear range, we need to pair up values
343
+ # Take the minimum length (they should increment together)
344
+ n = min(len(re_values), len(im_values))
345
+
346
+ if n == 0:
347
+ return []
348
+
349
+ return [_make_complex(re_values[i], im_values[i]) for i in range(n)]
350
+
351
+
352
+ def complex_range(
353
+ z1: Union[complex, float, int, List, Tuple],
354
+ z2: Optional[Union[complex, float, int]] = None,
355
+ step: Optional[Union[complex, float, int, List, Tuple]] = None,
356
+ *,
357
+ increment_first: Literal['im', 're'] = 'im',
358
+ farey_range: bool = False
359
+ ) -> List[complex]:
360
+ """
361
+ Generate a range of complex numbers.
362
+
363
+ This function can generate either rectangular (grid) or linear (diagonal)
364
+ ranges of complex numbers in the complex plane.
365
+
366
+ Parameters
367
+ ----------
368
+ z1 : complex, number, or list
369
+ For rectangular range: the first corner (or single endpoint if z2 is None).
370
+ For linear range: a list [z] or [z_start, z_end] specifying endpoints.
371
+ z2 : complex or number, optional
372
+ For rectangular range: the second corner.
373
+ step : complex, number, list, or tuple, optional
374
+ Step size(s) for the range.
375
+ - For rectangular range with complex step: real part = real step, imag = imag step
376
+ - For rectangular/linear range with list/tuple: [real_step, imag_step]
377
+ Default step is 1+1j for rectangular, (1, 1) for linear.
378
+ increment_first : {'im', 're'}, keyword-only
379
+ For rectangular range: which component to increment first.
380
+ 'im' (default): increment imaginary first (column-major order)
381
+ 're': increment real first (row-major order)
382
+ farey_range : bool, keyword-only
383
+ If True, use Farey sequence to generate intermediate points.
384
+ Only valid for rectangular ranges with integer step.
385
+ Creates a finer grid based on the Farey sequence of the given order.
386
+
387
+ Returns
388
+ -------
389
+ List[complex]
390
+ A list of complex numbers.
391
+
392
+ Raises
393
+ ------
394
+ ComplexRangeError
395
+ If farey_range is True but step is non-integer.
396
+
397
+ Examples
398
+ --------
399
+ Rectangular range from origin:
400
+
401
+ >>> complex_range(2+2j)
402
+ [(0+0j), 1j, 2j, (1+0j), (1+1j), (1+2j), (2+0j), (2+1j), (2+2j)]
403
+
404
+ Rectangular range between two points:
405
+
406
+ >>> complex_range(1+1j, 2+2j)
407
+ [(1+1j), (1+2j), (2+1j), (2+2j)]
408
+
409
+ Rectangular range with step:
410
+
411
+ >>> complex_range(0, 2+2j, 0.5+0.5j)
412
+ [(0+0j), 0.5j, 1j, 1.5j, 2j, (0.5+0j), ...]
413
+
414
+ Linear range (points on a diagonal):
415
+
416
+ >>> complex_range([0, 2+2j])
417
+ [(0+0j), (1+1j), (2+2j)]
418
+
419
+ Linear range with step:
420
+
421
+ >>> complex_range([0, 4+4j], (2, 2))
422
+ [(0+0j), (2+2j), (4+4j)]
423
+
424
+ Linear range with complex step:
425
+
426
+ >>> complex_range([0, 4+4j], 2+2j)
427
+ [(0+0j), (2+2j), (4+4j)]
428
+
429
+ Descending linear range:
430
+
431
+ >>> complex_range([2+2j, 0], -1-1j)
432
+ [(2+2j), (1+1j), 0j]
433
+
434
+ Rectangular range with Farey subdivision:
435
+
436
+ >>> complex_range(0, 1+1j, 2, farey_range=True)
437
+ [(0+0j), 0.5j, 1j, (0.5+0j), (0.5+0.5j), (0.5+1j), (1+0j), (1+0.5j), (1+1j)]
438
+
439
+ See Also
440
+ --------
441
+ farey_sequence : Generate Farey sequence of a given order.
442
+
443
+ Notes
444
+ -----
445
+ The function distinguishes between rectangular and linear ranges based on
446
+ the type of the first argument:
447
+
448
+ - If z1 is a list or tuple, a linear range is generated
449
+ - Otherwise, a rectangular range is generated
450
+
451
+ For rectangular ranges, the default iteration order is "imaginary first",
452
+ meaning for each real value, all imaginary values are enumerated before
453
+ moving to the next real value.
454
+
455
+ Author: Daniele Gregori
456
+ """
457
+ # Check if this is a linear range (first argument is a list/tuple)
458
+ if isinstance(z1, (list, tuple)):
459
+ return _handle_linear_range(z1, z2, step, farey_range)
460
+
461
+ # Rectangular range
462
+ return _handle_rectangular_range(z1, z2, step, increment_first, farey_range)
463
+
464
+
465
+ def _handle_linear_range(
466
+ z_spec: Union[List, Tuple],
467
+ step_or_none: Optional[Union[complex, float, int, List, Tuple]],
468
+ step: Optional[Union[complex, float, int, List, Tuple]],
469
+ farey_range: bool = False
470
+ ) -> List[complex]:
471
+ """Handle linear range cases."""
472
+ # Parse the specification
473
+ if len(z_spec) == 1:
474
+ # {z} - linear from 0 to z
475
+ z_start = complex(0, 0)
476
+ z_end = complex(z_spec[0])
477
+ elif len(z_spec) == 2:
478
+ # {z1, z2} - linear from z1 to z2
479
+ z_start = complex(z_spec[0])
480
+ z_end = complex(z_spec[1])
481
+ else:
482
+ raise ComplexRangeError(f"Linear range specification must have 1 or 2 elements, got {len(z_spec)}")
483
+
484
+ # Determine step (step_or_none is actually the step for linear ranges)
485
+ if step_or_none is not None:
486
+ if isinstance(step_or_none, (list, tuple)):
487
+ linear_step = tuple(step_or_none)
488
+ elif isinstance(step_or_none, complex):
489
+ # Pass complex step directly - _linear_range will handle it
490
+ linear_step = step_or_none
491
+ else:
492
+ raise ComplexRangeError(f"Invalid step specification: {step_or_none}")
493
+ else:
494
+ linear_step = None
495
+
496
+ return _linear_range(z_start, z_end, linear_step, farey_range)
497
+
498
+
499
+ def _handle_rectangular_range(
500
+ z1: Union[complex, float, int],
501
+ z2: Optional[Union[complex, float, int]],
502
+ step: Optional[Union[complex, float, int, List, Tuple]],
503
+ increment_first: Literal['im', 're'],
504
+ farey_range: bool
505
+ ) -> List[complex]:
506
+ """Handle rectangular range cases."""
507
+ # Convert to complex
508
+ z1_complex = complex(z1)
509
+
510
+ if z2 is None:
511
+ # Single argument: range from 0 to z1
512
+ z2_complex = z1_complex
513
+ z1_complex = complex(0, 0)
514
+ else:
515
+ z2_complex = complex(z2)
516
+
517
+ # Handle step
518
+ if step is not None:
519
+ if isinstance(step, (list, tuple)):
520
+ step_tuple = tuple(step)
521
+ else:
522
+ step_val = complex(step) if not isinstance(step, complex) else step
523
+ step_tuple = step_val
524
+ else:
525
+ step_tuple = None
526
+
527
+ return _rectangular_range(z1_complex, z2_complex, step_tuple, increment_first, farey_range)
528
+
529
+
530
+ # Convenience function for iterator version
531
+ def complex_range_iter(
532
+ z1: Union[complex, float, int, List, Tuple],
533
+ z2: Optional[Union[complex, float, int]] = None,
534
+ step: Optional[Union[complex, float, int, List, Tuple]] = None,
535
+ *,
536
+ increment_first: Literal['im', 're'] = 'im',
537
+ farey_range: bool = False
538
+ ) -> Iterator[complex]:
539
+ """
540
+ Iterator version of complex_range.
541
+
542
+ Same as complex_range but returns an iterator instead of a list.
543
+ Useful for memory efficiency with large ranges.
544
+
545
+ See complex_range for full documentation.
546
+
547
+ Author: Daniele Gregori
548
+ """
549
+ return iter(complex_range(z1, z2, step, increment_first=increment_first, farey_range=farey_range))
complex_range/dev.py ADDED
@@ -0,0 +1,41 @@
1
+ """
2
+ Development/testing utilities for complex-range.
3
+
4
+ This module exposes internal functions for interactive testing and debugging.
5
+ Import with: from complex_range.dev import *
6
+
7
+ Author: Daniele Gregori
8
+ """
9
+
10
+ from .core import (
11
+ complex_range,
12
+ complex_range_iter,
13
+ ComplexRangeError,
14
+ _rectangular_range,
15
+ _linear_range,
16
+ _arange_inclusive,
17
+ _make_complex,
18
+ _farey_range_values,
19
+ _handle_linear_range,
20
+ _handle_rectangular_range,
21
+ _to_number,
22
+ )
23
+ from .farey import farey_sequence, scaled_farey_sequence
24
+
25
+ __all__ = [
26
+ # Public API
27
+ "complex_range",
28
+ "complex_range_iter",
29
+ "ComplexRangeError",
30
+ "farey_sequence",
31
+ # Internal functions (for testing)
32
+ "_rectangular_range",
33
+ "_linear_range",
34
+ "_arange_inclusive",
35
+ "_make_complex",
36
+ "_farey_range_values",
37
+ "_handle_linear_range",
38
+ "_handle_rectangular_range",
39
+ "_to_number",
40
+ "scaled_farey_sequence",
41
+ ]
complex_range/farey.py ADDED
@@ -0,0 +1,84 @@
1
+ """
2
+ Farey sequence implementation for complex-range.
3
+
4
+ The Farey sequence F_n is the sequence of completely reduced fractions,
5
+ between 0 and 1, which have denominators less than or equal to n,
6
+ arranged in order of increasing size.
7
+
8
+ Author: Daniele Gregori
9
+ """
10
+
11
+ from fractions import Fraction
12
+ from typing import List
13
+
14
+
15
+ def farey_sequence(n: int) -> List[Fraction]:
16
+ """
17
+ Generate the Farey sequence of order n.
18
+
19
+ The Farey sequence F_n is the sequence of completely reduced fractions
20
+ between 0 and 1, with denominators less than or equal to n, arranged
21
+ in increasing order.
22
+
23
+ Parameters
24
+ ----------
25
+ n : int
26
+ The order of the Farey sequence. Must be a positive integer.
27
+
28
+ Returns
29
+ -------
30
+ List[Fraction]
31
+ A list of Fraction objects representing the Farey sequence.
32
+
33
+ Raises
34
+ ------
35
+ ValueError
36
+ If n is not a positive integer.
37
+
38
+ Examples
39
+ --------
40
+ >>> farey_sequence(1)
41
+ [Fraction(0, 1), Fraction(1, 1)]
42
+
43
+ >>> farey_sequence(3)
44
+ [Fraction(0, 1), Fraction(1, 3), Fraction(1, 2), Fraction(2, 3), Fraction(1, 1)]
45
+
46
+ >>> farey_sequence(5)
47
+ [Fraction(0, 1), Fraction(1, 5), Fraction(1, 4), Fraction(1, 3),
48
+ Fraction(2, 5), Fraction(1, 2), Fraction(3, 5), Fraction(2, 3),
49
+ Fraction(3, 4), Fraction(4, 5), Fraction(1, 1)]
50
+ """
51
+ if not isinstance(n, int) or n < 1:
52
+ raise ValueError(f"Order n must be a positive integer, got {n}")
53
+
54
+ # Generate all fractions with denominator <= n
55
+ fractions_set = set()
56
+ for denom in range(1, n + 1):
57
+ for numer in range(0, denom + 1):
58
+ fractions_set.add(Fraction(numer, denom))
59
+
60
+ # Sort and return
61
+ return sorted(fractions_set)
62
+
63
+
64
+ def scaled_farey_sequence(n: int, start: float, end: float) -> List[Fraction]:
65
+ """
66
+ Generate a scaled Farey sequence between start and end.
67
+
68
+ Parameters
69
+ ----------
70
+ n : int
71
+ The order of the Farey sequence.
72
+ start : float
73
+ The starting value.
74
+ end : float
75
+ The ending value.
76
+
77
+ Returns
78
+ -------
79
+ List[Fraction]
80
+ Farey sequence scaled to [start, end] interval.
81
+ """
82
+ farey = farey_sequence(n)
83
+ span = end - start
84
+ return [Fraction(start) + f * Fraction(span) for f in farey]
complex_range/py.typed ADDED
File without changes
@@ -0,0 +1,330 @@
1
+ Metadata-Version: 2.4
2
+ Name: complex-range
3
+ Version: 1.0.0
4
+ Summary: Generate ranges of complex numbers - rectangular grids and linear sequences in the complex plane
5
+ Author-email: Daniele Gregori <dangregori@gmail.com>
6
+ Maintainer-email: Daniele Gregori <dangregori@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://pypi.org/project/complex-range
9
+ Project-URL: Documentation, https://resources.wolframcloud.com/FunctionRepository/resources/ComplexRange/
10
+ Project-URL: Repository, https://github.com/Daniele-Gregori/PyPI-packages
11
+ Project-URL: Issues, https://github.com/Daniele-Gregori/PyPI-packages/issues
12
+ Project-URL: Changelog, https://github.com/Daniele-Gregori/PyPI-packages/blob/main/CHANGELOG.md
13
+ Keywords: complex,numbers,range,grid,mathematics,farey,sequence,complex-plane
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Intended Audience :: Education
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
27
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.8
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
35
+ Requires-Dist: black>=23.0; extra == "dev"
36
+ Requires-Dist: isort>=5.0; extra == "dev"
37
+ Requires-Dist: mypy>=1.0; extra == "dev"
38
+ Requires-Dist: flake8>=6.0; extra == "dev"
39
+ Provides-Extra: docs
40
+ Requires-Dist: sphinx>=6.0; extra == "docs"
41
+ Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
42
+ Dynamic: license-file
43
+
44
+ # complex-range
45
+
46
+ [![PyPI version](https://badge.fury.io/py/complex-range.svg)](https://badge.fury.io/py/complex-range)
47
+ [![Python versions](https://img.shields.io/pypi/pyversions/complex-range.svg)](https://pypi.org/project/complex-range/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
49
+
50
+
51
+ Generate ranges of complex numbers in Python - rectangular grids and linear sequences in the complex plane.
52
+
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install complex-range
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ```python
63
+ from complex_range import complex_range
64
+
65
+ # Rectangular grid from origin to 2+2j
66
+ grid = complex_range(0, 2+2j)
67
+ # [0j, 1j, 2j, (1+0j), (1+1j), (1+2j), (2+0j), (2+1j), (2+2j)]
68
+
69
+ # Linear range along a diagonal
70
+ line = complex_range([0, 3+3j])
71
+ # [0j, (1+1j), (2+2j), (3+3j)]
72
+ ```
73
+
74
+ ## Features
75
+
76
+ - **Rectangular ranges**: Generate 2D grids of complex numbers
77
+ - **Linear ranges**: Generate points along a line in the complex plane
78
+ - **Flexible step sizes**: Control spacing independently for real and imaginary axes
79
+ - **Negative steps**: Support for descending ranges with negative step values
80
+ - **Farey sequence subdivision**: Create refined grids using number-theoretic sequences
81
+ - **Iteration order control**: Choose whether to iterate real or imaginary component first
82
+ - **Pure Python**: No dependencies required
83
+
84
+ ## Usage
85
+
86
+ ### Rectangular Ranges
87
+
88
+ Generate a grid of complex numbers between two corners:
89
+
90
+ ```python
91
+ from complex_range import complex_range
92
+
93
+ # Basic rectangular range
94
+ complex_range(0, 2+2j)
95
+ # Returns: [0j, 1j, 2j, (1+0j), (1+1j), (1+2j), (2+0j), (2+1j), (2+2j)]
96
+
97
+ # Single argument: range from 0 to z
98
+ complex_range(2+3j)
99
+ # Returns grid from 0 to 2+3j (3×4 = 12 points)
100
+
101
+ # With custom step size (complex number)
102
+ complex_range(0, 2+2j, 0.5+0.5j)
103
+ # Real step = 0.5, Imaginary step = 0.5
104
+
105
+ # With separate real and imaginary steps
106
+ complex_range(0, 4+6j, [2, 3])
107
+ # Real step = 2, Imaginary step = 3
108
+ ```
109
+
110
+ ### Linear Ranges
111
+
112
+ Generate points along a line (diagonal) in the complex plane:
113
+
114
+ ```python
115
+ # Linear from 0 to endpoint
116
+ complex_range([3+3j])
117
+ # Returns: [0j, (1+1j), (2+2j), (3+3j)]
118
+
119
+ # Linear between two points
120
+ complex_range([-1-1j, 2+2j])
121
+ # Returns: [(-1-1j), 0j, (1+1j), (2+2j)]
122
+
123
+ # Linear with custom step
124
+ complex_range([0, 4+4j], [2, 2])
125
+ # Returns: [0j, (2+2j), (4+4j)]
126
+
127
+ # Linear with complex step
128
+ complex_range([0, 4+4j], 2+2j)
129
+ # Returns: [0j, (2+2j), (4+4j)]
130
+
131
+ # Descending linear range with negative step
132
+ complex_range([2+2j, 0], -1-1j)
133
+ # Returns: [(2+2j), (1+1j), 0j]
134
+ ```
135
+
136
+ ### Options
137
+
138
+ #### `increment_first`
139
+
140
+ Control the iteration order for rectangular ranges:
141
+
142
+ ```python
143
+ # Default: increment imaginary first
144
+ complex_range(0, 2+2j, 1+1j, increment_first='im')
145
+ # [0j, 1j, 2j, (1+0j), (1+1j), (1+2j), (2+0j), (2+1j), (2+2j)]
146
+
147
+ # Increment real first
148
+ complex_range(0, 2+2j, 1+1j, increment_first='re')
149
+ # [0j, (1+0j), (2+0j), 1j, (1+1j), (2+1j), 2j, (1+2j), (2+2j)]
150
+ ```
151
+
152
+ #### `farey_range`
153
+
154
+ Use Farey sequence to create a finer subdivision of the grid:
155
+
156
+ ```python
157
+ # Regular grid
158
+ regular = complex_range(0, 4+4j, 2+2j)
159
+ # Returns 9 points
160
+
161
+ # Farey subdivision (step must be integer)
162
+ farey = complex_range(0, 4+4j, 2+2j, farey_range=True)
163
+ # Returns 81 points using Farey sequence F_2
164
+
165
+ # Farey sequence of order n creates fractions 0, 1/n, ..., 1
166
+ from complex_range import farey_sequence
167
+ farey_sequence(3)
168
+ # [Fraction(0,1), Fraction(1,3), Fraction(1,2), Fraction(2,3), Fraction(1,1)]
169
+ ```
170
+
171
+ ## API Reference
172
+
173
+ ### `complex_range(z1, z2=None, step=None, *, increment_first='im', farey_range=False)`
174
+
175
+ Generate a range of complex numbers.
176
+
177
+ **Parameters:**
178
+
179
+ | Parameter | Type | Description |
180
+ |-----------|------|-------------|
181
+ | `z1` | `complex`, `list`, or `tuple` | First corner (rectangular) or endpoint specification (linear) |
182
+ | `z2` | `complex`, optional | Second corner for rectangular range |
183
+ | `step` | `complex`, `list`, or `tuple`, optional | Step size(s). Default: `1+1j` |
184
+ | `increment_first` | `'im'` or `'re'` | Which component to iterate first. Default: `'im'` |
185
+ | `farey_range` | `bool` | Use Farey sequence subdivision. Default: `False` |
186
+
187
+ **Returns:** `List[complex]` - List of complex numbers
188
+
189
+ **Raises:** `ComplexRangeError` - If `farey_range=True` with non-integer step
190
+
191
+ ### `farey_sequence(n)`
192
+
193
+ Generate the Farey sequence of order n.
194
+
195
+ **Parameters:**
196
+
197
+ | Parameter | Type | Description |
198
+ |-----------|------|-------------|
199
+ | `n` | `int` | Order of the Farey sequence (must be positive) |
200
+
201
+ **Returns:** `List[Fraction]` - Sorted list of fractions in [0, 1]
202
+
203
+ ## Understanding the Ranges
204
+
205
+ ### Rectangular Range
206
+
207
+ A rectangular range generates all points on a 2D grid:
208
+
209
+ ```
210
+ Im ↑
211
+ 3 │ · · ·
212
+ 2 │ · · ·
213
+ 1 │ · · ·
214
+ 0 │ · · ·
215
+ └──────────→ Re
216
+ 0 1 2
217
+ ```
218
+
219
+ With `increment_first='im'` (default), points are generated column by column:
220
+ `(0,0), (0,1), (0,2), (0,3), (1,0), (1,1), ...`
221
+
222
+ ### Linear Range
223
+
224
+ A linear range generates points along a diagonal:
225
+
226
+ ```
227
+ Im ↑
228
+ 3 │ ·
229
+ 2 │ ·
230
+ 1 │ ·
231
+ 0 │·
232
+ └──────────→ Re
233
+ 0 1 2 3
234
+ ```
235
+
236
+ Points increment along both axes simultaneously.
237
+
238
+ ## Examples
239
+
240
+ ### Visualizing a Rectangular Grid
241
+
242
+ ```python
243
+ import matplotlib.pyplot as plt
244
+ from complex_range import complex_range
245
+
246
+ points = complex_range(0, 3+3j)
247
+ x = [p.real for p in points]
248
+ y = [p.imag for p in points]
249
+
250
+ plt.scatter(x, y)
251
+ plt.xlabel('Real')
252
+ plt.ylabel('Imaginary')
253
+ plt.title('Rectangular Complex Range')
254
+ plt.grid(True)
255
+ plt.show()
256
+ ```
257
+
258
+ ### Creating a Refined Grid with Farey Sequence
259
+
260
+ ```python
261
+ from complex_range import complex_range
262
+
263
+ # Coarse grid: 3×3 = 9 points
264
+ coarse = complex_range(0, 2+2j, 1+1j)
265
+
266
+ # Refined grid using Farey sequence
267
+ refined = complex_range(0, 2+2j, 2+2j, farey_range=True)
268
+ print(f"Coarse: {len(coarse)} points, Refined: {len(refined)} points")
269
+ ```
270
+
271
+ ### Iterating Over Complex Regions
272
+
273
+ ```python
274
+ from complex_range import complex_range
275
+
276
+ # Evaluate a function over a region of the complex plane
277
+ def mandelbrot_escape(c, max_iter=100):
278
+ z = 0
279
+ for i in range(max_iter):
280
+ z = z*z + c
281
+ if abs(z) > 2:
282
+ return i
283
+ return max_iter
284
+
285
+ # Create a grid and evaluate
286
+ grid = complex_range(-2-1.5j, 1+1.5j, [0.1, 0.1])
287
+ escapes = [mandelbrot_escape(c) for c in grid]
288
+ ```
289
+
290
+ ## Author
291
+
292
+ **Daniele Gregori**
293
+
294
+ - Original [Wolfram Language ComplexRange](https://resources.wolframcloud.com/FunctionRepository/resources/ComplexRange/) function
295
+ - Python implementation
296
+
297
+ ## Contributing
298
+
299
+ Contributions are welcome! Please feel free to submit a Pull Request.
300
+
301
+ ```bash
302
+ # Clone the repository
303
+ git clone https://github.com/Daniele-Gregori/PyPI-packages.git
304
+ cd PyPI-packages/packages/complex-range
305
+
306
+ # Install development dependencies
307
+ pip install -e ".[dev]"
308
+
309
+ # Run tests
310
+ pytest
311
+
312
+ # Run type checking
313
+ mypy src/complex_range
314
+
315
+ # Format code
316
+ black src tests
317
+ isort src tests
318
+ ```
319
+
320
+ ## License
321
+
322
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
323
+
324
+ ## Acknowledgments
325
+
326
+ Original [Wolfram Language ComplexRange](https://resources.wolframcloud.com/FunctionRepository/resources/ComplexRange/) resource function contributed by Daniele Gregori and reviewed by the Wolfram Review Team.
327
+
328
+ ## Changelog
329
+
330
+ See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
@@ -0,0 +1,10 @@
1
+ complex_range/__init__.py,sha256=hZC54Rv4f2Zvha7IDM-DngPqqwDYFugErkzGSLir7-Y,779
2
+ complex_range/core.py,sha256=pgq185F5KX3hjju7k4bGJoeMegarncoAHx2-Y20wd-Y,18196
3
+ complex_range/dev.py,sha256=I2uaDDWVMJoDfdRPZin-0XTKElRz3ianknT_7oOrPws,929
4
+ complex_range/farey.py,sha256=xDcZSJi1h2vMtghkP8TbaEc2dqHbGc4_C7UaD1H6qS0,2305
5
+ complex_range/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ complex_range-1.0.0.dist-info/licenses/LICENSE,sha256=S2_QCHbwpFs9uShfKzzbIYGKLwPhrlcUUVZnpieCdDU,1072
7
+ complex_range-1.0.0.dist-info/METADATA,sha256=zcL9a78XhglVg2qIR1emviUJr7h8ey1tJFxtM-Kh-b4,9311
8
+ complex_range-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ complex_range-1.0.0.dist-info/top_level.txt,sha256=AjyB3ZnFm0s9ReE4FvZzVKfwOJ62TI7NU_ieImIlutk,14
10
+ complex_range-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daniele Gregori
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ complex_range