simetri.helpers.utilities
Simetri graphics library's utility functions.
1"""Simetri graphics library's utility functions.""" 2 3import collections 4import os 5import re 6import base64 7import cmath 8from functools import wraps, reduce 9from time import time, monotonic, perf_counter 10from math import factorial, cos, sin, pi, atan2, sqrt 11from pathlib import Path 12from bisect import bisect_left 13 14from typing import Sequence 15 16from PIL import ImageFont 17from numpy import array, ndarray 18import numpy as np 19from numpy import isclose 20 21from ..settings.settings import defaults 22from ..graphics.common import get_defaults, Point, Line 23 24 25def time_it(func): 26 """Decorator to time a function""" 27 28 @wraps(func) 29 def time_it_wrapper(*args, **kwargs): 30 start_time = perf_counter() 31 result = func(*args, **kwargs) 32 end_time = perf_counter() 33 total_time = end_time - start_time 34 print(f"Function {func.__name__} Took {total_time:.6f} seconds") 35 return result 36 37 return time_it_wrapper 38 39 40def close_logger(logger): 41 """Close the logger and remove all handlers. 42 43 Args: 44 logger: The logger instance to close. 45 """ 46 for handler in logger.handlers: 47 handler.close() 48 logger.removeHandler(handler) 49 50 51def get_file_path_with_rev(directory, script_path, ext=".pdf"): 52 """Get the file path with a revision number. 53 54 Args: 55 directory: The directory to search for files. 56 script_path: The script file path. 57 ext: The file extension. 58 59 Returns: 60 The file path with a revision number. 61 """ 62 63 # Get the file path of the script 64 def get_rev_number(file_name): 65 match = re.search(r"_\d+$", file_name) 66 if match: 67 rev = match.group()[1:] # remove the underscore 68 if rev is not None: 69 return int(rev) 70 return 0 71 72 # script_path = __file__ 73 filename = os.path.basename(script_path) 74 filename, _ = os.path.splitext(filename) 75 # check if the file is in the current directory 76 files = os.listdir(directory) 77 file_names = [ 78 os.path.splitext(item)[0] 79 for item in files 80 if os.path.isfile(os.path.join(directory, item)) 81 ] 82 existing = [item for item in file_names if item.startswith(filename)] 83 if not existing: 84 return os.path.join(directory, filename + ext) 85 86 revs = [get_rev_number(file) for file in existing] 87 if revs is None: 88 rev = 1 89 else: 90 rev = max(revs) + 1 91 92 return os.path.join(directory, f"{filename}_{rev}" + ext) 93 94 95def remove_file_handler(logger, handler): 96 """Remove a handler from a logger. 97 98 Args: 99 logger: The logger instance. 100 handler: The handler to remove. 101 """ 102 logger.removeHandler(handler) 103 handler.close() 104 105 106def pretty_print_coords(coords: Sequence[Point]) -> str: 107 """Print the coordinates with a precision of 2. 108 109 Args: 110 coords: A sequence of Point objects. 111 112 Returns: 113 A string representation of the coordinates. 114 """ 115 return ( 116 "(" + ", ".join([f"({coord[0]:.2f}, {coord[1]:.2f})" for coord in coords]) + ")" 117 ) 118 119 120def is_file_empty(file_path): 121 """Check if a file is empty. 122 123 Args: 124 file_path: The path to the file. 125 126 Returns: 127 True if the file is empty, False otherwise. 128 """ 129 return os.path.getsize(file_path) == 0 130 131 132def wait_for_file_availability(file_path, timeout=None, check_interval=1): 133 """Check if a file is available for writing. 134 135 Args: 136 file_path: The path to the file. 137 timeout: The timeout period in seconds. 138 check_interval: The interval to check the file availability. 139 140 Returns: 141 True if the file is available, False otherwise. 142 """ 143 start_time = monotonic() 144 while True: 145 try: 146 # Attempt to open the file in write mode. This will raise an exception 147 # if the file is currently locked or being written to. 148 with open(file_path, "a", encoding="utf-8"): 149 # If the file was successfully opened, it's available. 150 return True 151 except IOError: 152 # The file is likely in use. 153 if timeout is not None and (monotonic() - start_time) > timeout: 154 # Timeout period elapsed. 155 return False # Or raise a TimeoutError if you prefer 156 time.sleep(check_interval) 157 except Exception as e: 158 # Handle other potential exceptions (e.g., file not found) as needed 159 print(f"An error occurred: {e}") 160 return False 161 162 163def detokenize(text: str) -> str: 164 """Replace the special Latex characters with their Latex commands. 165 166 Args: 167 text: The text to detokenize. 168 169 Returns: 170 The detokenized text. 171 """ 172 if text.startswith("$") and text.endswith("$"): 173 res = text 174 else: 175 replacements = { 176 "\\": r"\textbackslash ", 177 "{": r"\{", 178 "}": r"\}", 179 "$": r"\$", 180 "&": r"\&", 181 "%": r"\%", 182 "#": r"\#", 183 "_": r"\_", 184 "^": r"\^{}", 185 "~": r"\textasciitilde{}", 186 } 187 for char, replacement in replacements.items(): 188 text = text.replace(char, replacement) 189 res = text 190 191 return res 192 193 194def get_text_dimensions(text, font_path, font_size): 195 """Return the width and height of the text. 196 197 Args: 198 text: The text to measure. 199 font_path: The path to the font file. 200 font_size: The size of the font. 201 202 Returns: 203 A tuple containing the width and height of the text. 204 """ 205 font = ImageFont.truetype(font_path, font_size) 206 _, descent = font.getmetrics() 207 text_width = font.getmask(text).getbbox()[2] 208 text_height = font.getmask(text).getbbox()[3] + descent 209 return text_width, text_height 210 211 212def timing(func): 213 """Print the execution time of a function. 214 215 Args: 216 func: The function to time. 217 218 Returns: 219 The wrapped function. 220 """ 221 222 @wraps(func) 223 def wrap(*args, **kw): 224 start_time = time() 225 result = func(*args, **kw) 226 end_time = time() 227 elapsed_time = end_time - start_time 228 print(f"function:{func.__name__} took: {elapsed_time:.4f} sec") 229 230 return result 231 232 return wrap 233 234 235def find_nearest_value(values: array, value: float) -> float: 236 """Find the closest value in an array to a given number. 237 238 Args: 239 values: A NumPy array. 240 value: The number to find the closest value to. 241 242 Returns: 243 The closest value in the array to the given number. 244 """ 245 arr = np.asarray(values) 246 idx = (np.abs(arr - value)).argmin() 247 248 return arr[idx] 249 250 251def nested_count(nested_sequence): 252 """Return the total number of items in a nested sequence. 253 254 Args: 255 nested_sequence: A nested sequence. 256 257 Returns: 258 The total number of items in the nested sequence. 259 """ 260 return sum( 261 nested_count(item) if isinstance(item, (list, tuple, ndarray)) else 1 262 for item in nested_sequence 263 ) 264 265 266def decompose_transformations(transformation_matrix): 267 """Decompose a 3x3 transformation matrix into translation, rotation, and scale components. 268 269 Args: 270 transformation_matrix: A 3x3 transformation matrix. 271 272 Returns: 273 A tuple containing the translation, rotation, and scale components. 274 """ 275 xform = transformation_matrix 276 translation = xform[2, :2] 277 rotation = np.arctan2(xform[0, 1], xform[0, 0]) 278 scale = np.linalg.norm(xform[:2, 0]), np.linalg.norm(xform[:2, 1]) 279 280 return translation, rotation, scale 281 282 283def check_directory(dir_path): 284 """Check if a directory is valid and writable. 285 286 Args: 287 dir_path: The path to the directory. 288 289 Returns: 290 A tuple containing a boolean indicating validity and an error message. 291 """ 292 error_msg = [] 293 294 def dir_exists(): 295 nonlocal error_msg 296 parent_dir = os.path.dirname(dir_path) 297 if not os.path.exists(parent_dir): 298 error_msg.append("Error! Parent directory doesn't exist") 299 300 def is_writable(): 301 nonlocal error_msg 302 parent_dir = os.path.dirname(dir_path) 303 if not os.access(parent_dir, os.W_OK): 304 error_msg.append("Error! Path is not writable.") 305 306 dir_exists() 307 is_writable() 308 if error_msg: 309 res = False, "\n".join(error_msg) 310 else: 311 res = True, "" 312 313 return res 314 315 316def analyze_path(file_path, overwrite): 317 """Check if a file path is valid and writable. 318 319 Args: 320 file_path: The path to the file. 321 overwrite: Whether to overwrite the file if it exists. 322 323 Returns: 324 A tuple containing a boolean indicating validity, the file extension, and an error message. 325 """ 326 supported_types = (".pdf", ".svg", ".ps", ".eps", ".tex") 327 error_msg = "" 328 329 def is_writable(): 330 nonlocal error_msg 331 parent_dir = os.path.dirname(file_path) 332 if os.access(parent_dir, os.W_OK): 333 res = True 334 else: 335 error_msg = "Error! Path is not writable." 336 res = False 337 338 return res 339 340 def is_supported(): 341 nonlocal error_msg 342 extension = Path(file_path).suffix 343 if extension in supported_types: 344 res = True 345 else: 346 error_msg = f"Error! Only {', '.join(supported_types)} supported." 347 res = False 348 349 return res 350 351 def can_overwrite(overwrite): 352 nonlocal error_msg 353 if os.path.exists(file_path): 354 if overwrite is None: 355 overwrite = defaults["overwrite_files"] 356 if overwrite: 357 res = True 358 else: 359 error_msg = ( 360 "Error! File exists. Use canvas." 361 "save(f_path, overwrite=True) to overwrite." 362 ) 363 res = False 364 else: 365 res = True 366 367 return res 368 369 try: 370 file_path = os.path.abspath(file_path) 371 if is_writable() and is_supported() and can_overwrite(overwrite): 372 res = (True, "", Path(file_path).suffix) 373 else: 374 res = (False, error_msg, "") 375 376 return res 377 except ( 378 Exception 379 ) as e: # Million other ways a file path is not valid but life is short! 380 return False, f"Path Error! {e}", "" 381 382 383def can_be_xform_matrix(seq): 384 """Check if a sequence can be converted to a transformation matrix. 385 386 Args: 387 seq: The sequence to check. 388 389 Returns: 390 True if the sequence can be converted to a transformation matrix, False otherwise. 391 """ 392 # check if it is a sequence that can be 393 # converted to a transformation matrix 394 try: 395 arr = array(seq) 396 return is_xform_matrix(arr) 397 except Exception: 398 return False 399 400 401def is_sequence(value): 402 """Check if a value is a sequence. 403 404 Args: 405 value: The value to check. 406 407 Returns: 408 True if the value is a sequence, False otherwise. 409 """ 410 return isinstance(value, (list, tuple, array)) 411 412 413def rel_coord(dx: float, dy: float, center: Point) -> Point: 414 """Return the relative coordinates. 415 416 Args: 417 dx: The x-coordinate difference. 418 dy: The y-coordinate difference. 419 center: The center coordinates. 420 421 Returns: 422 The relative coordinates. 423 """ 424 return dx + center[0], dy + center[1] 425 426 427def rel_polar(r: float, angle: float, center: Point) -> Point: 428 """Return the coordinates. 429 430 Args: 431 r: The radius. 432 angle: The angle in radians. 433 center: The center coordinates. 434 435 Returns: 436 The coordinates. 437 """ 438 x, y = center[:2] 439 x1 = x + r * cos(angle) 440 y1 = y + r * sin(angle) 441 442 return x1, y1 443 444 445rc = rel_coord # alias for rel_coord 446rp = rel_polar # alias for rel_polar 447 448def axis(angle: float, length: float = 10) -> Line: 449 """Return a line [(x1, y1), (x2, y2)] with the given angle 450 and length. 451 Args: 452 angle: The angle between the line and the x-axis, in radians. 453 length: The length of the line. 454 455 Returns: 456 A line represented as a tuple of two points. 457 """ 458 length2 = length / 2 459 x1 = cos(angle) * length2 460 y1 = sin(angle) * length2 461 x2 = -x1 462 y2 = -y1 463 464 return (x1, y1), (x2, y2) 465 466 467 468 469def flatten(points): 470 """Flatten the points and return it as a list. 471 472 Args: 473 points: A sequence of points. 474 475 Returns: 476 A flattened list of points. 477 """ 478 if isinstance(points, set): 479 points = list(points) 480 if isinstance(points, np.ndarray): 481 flat = list(points[:, :2].flatten()) 482 elif isinstance(points, collections.abc.Sequence): 483 if isinstance(points[0], collections.abc.Sequence): 484 flat = list(reduce(lambda x, y: x + y, [list(pnt[:2]) for pnt in points])) 485 else: 486 flat = list(points) 487 else: 488 raise TypeError("Error! Invalid data type.") 489 490 return flat 491 492 493def find_closest_value(a_sorted_list, value): 494 """Return the index of the closest value and the value itself in a sorted list. 495 496 Args: 497 a_sorted_list: A sorted list of values. 498 value: The value to find the closest match for. 499 500 Returns: 501 A tuple containing the closest value and its index. 502 """ 503 ind = bisect_left(a_sorted_list, value) 504 505 if ind == 0: 506 return a_sorted_list[0] 507 508 if ind == len(a_sorted_list): 509 return a_sorted_list[-1] 510 511 left = a_sorted_list[ind - 1] 512 right = a_sorted_list[ind] 513 514 if right - value < value - left: 515 return right, ind 516 else: 517 return left, ind - 1 518 519 520def value_from_intervals(value, values, intervals): 521 """Return the value from the intervals. 522 Args: 523 value: The value to find. 524 values: The values to search. 525 intervals: The intervals to search. 526 Returns: 527 The value from the intervals. 528 """ 529 530 return values[bisect_left(intervals, value)] 531 532 533def get_transform(transform): 534 """Return the transformation matrix. 535 536 Args: 537 transform: The transformation matrix or sequence. 538 539 Returns: 540 The transformation matrix. 541 """ 542 if transform is None: 543 # return identity 544 res = array([[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]]) 545 else: 546 if is_xform_matrix(transform): 547 res = transform 548 elif can_be_xform_matrix(transform): 549 res = array(transform) 550 else: 551 raise RuntimeError("Invalid transformation matrix!") 552 return res 553 554 555def is_numeric_numpy_array(array_): 556 """Check if it is an array of numbers. 557 558 Args: 559 array_: The array to check. 560 561 Returns: 562 True if the array is numeric, False otherwise. 563 """ 564 if not isinstance(array_, np.ndarray): 565 return False 566 567 numeric_types = { 568 "u", # unsigned integer 569 "i", # signed integer 570 "f", # floating-point 571 "c", 572 } # complex number 573 try: 574 return array_.dtype.kind in numeric_types 575 except AttributeError: 576 return False 577 578 579def is_xform_matrix(matrix): 580 """Check if it is a 3x3 transformation matrix. 581 582 Args: 583 matrix: The matrix to check. 584 585 Returns: 586 True if the matrix is a 3x3 transformation matrix, False otherwise. 587 """ 588 return ( 589 is_numeric_numpy_array(matrix) and matrix.shape == (3, 3) and matrix.size == 9 590 ) 591 592 593def prime_factors(n): 594 """Prime factorization. 595 596 Args: 597 n: The number to factorize. 598 599 Returns: 600 A list of prime factors. 601 """ 602 p = 2 603 factors = [] 604 while n > 1: 605 if n % p: 606 p += 1 607 else: 608 factors.append(p) 609 n = n / p 610 return factors 611 612 613def random_id(): 614 """Generate a random ID. 615 616 Returns: 617 A random ID string. 618 """ 619 return base64.b64encode(os.urandom(6)).decode("ascii") 620 621 622def decompose_svg_transform(transform): 623 """Decompose a SVG transformation string. 624 625 Args: 626 transform: The SVG transformation string. 627 628 Returns: 629 A tuple containing the decomposed transformation components. 630 """ 631 a, b, c, d, e, f = transform 632 # [[a, c, e], 633 # [b, d, f], 634 # [0, 0, 1]] 635 dx = e 636 dy = f 637 638 sx = np.sign(a) * sqrt(a**2 + c**2) 639 sy = np.sign(d) * sqrt(b**2 + d**2) 640 641 angle = atan2(b, d) 642 643 return dx, dy, sx, sy, angle 644 645 646def abcdef_svg(transform_matrix): 647 """Return the a, b, c, d, e, f for SVG transformations. 648 649 Args: 650 transform_matrix: A Numpy array representing the transformation matrix. 651 652 Returns: 653 A tuple containing the a, b, c, d, e, f components. 654 """ 655 # [[a, c, e], 656 # [b, d, f], 657 # [0, 0, 1]] 658 a, b, _, c, d, _, e, f, _ = list(transform_matrix.flat) 659 return (a, b, c, d, e, f) 660 661 662def abcdef_pil(xform_matrix): 663 """Return the a, b, c, d, e, f for PIL transformations. 664 665 Args: 666 xform_matrix: A Numpy array representing the transformation matrix. 667 668 Returns: 669 A tuple containing the a, b, c, d, e, f components. 670 """ 671 a, d, _, b, e, _, c, f, _ = list(xform_matrix.flat) 672 return (a, b, c, d, e, f) 673 674 675def abcdef_reportlab(xform_matrix): 676 """Return the a, b, c, d, e, f for Reportlab transformations. 677 678 Args: 679 xform_matrix: A Numpy array representing the transformation matrix. 680 681 Returns: 682 A tuple containing the a, b, c, d, e, f components. 683 """ 684 # a, b, _, c, d, _, e, f, _ = list(np.transpose(xform_matrix).flat) 685 a, b, _, c, d, _, e, f, _ = list(xform_matrix.flat) 686 return (a, b, c, d, e, f) 687 688 689def lerp(start, end, t): 690 """Linear interpolation of two values. 691 692 Args: 693 start: The start value. 694 end: The end value. 695 t: The interpolation factor (0 <= t <= 1). 696 697 Returns: 698 The interpolated value. 699 """ 700 return start + t * (end - start) 701 702 703def inv_lerp(start, end, value): 704 """Inverse linear interpolation of two values. 705 706 Args: 707 start: The start value. 708 end: The end value. 709 value: The value to interpolate. 710 711 Returns: 712 The interpolation factor (0 <= t <= 1). 713 """ 714 return (value - start) / (end - start) 715 716 717def sanitize_weighted_graph_edges(edges): 718 """Sanitize weighted graph edges. 719 720 Args: 721 edges: A list of weighted graph edges. 722 723 Returns: 724 A sanitized list of weighted graph edges. 725 """ 726 clean_edges = [] 727 s_seen = set() 728 for edge in edges: 729 e1, e2, _ = edge 730 frozen_edge = frozenset((e1, e2)) 731 if frozen_edge in s_seen: 732 continue 733 s_seen.add(frozen_edge) 734 clean_edges.append(edge) 735 clean_edges.sort() 736 return clean_edges 737 738 739def sanitize_graph_edges(edges): 740 """Sanitize graph edges. 741 742 Args: 743 edges: A list of graph edges. 744 745 Returns: 746 A sanitized list of graph edges. 747 """ 748 s_edge_set = set() 749 for edge in edges: 750 s_edge_set.add(frozenset(edge)) 751 edges = [tuple(x) for x in s_edge_set] 752 edges = [(min(x), max(x)) for x in edges] 753 edges.sort() 754 return edges 755 756 757def flatten2(nested_list): 758 """Flatten a nested list. 759 760 Args: 761 nested_list: The nested list to flatten. 762 763 Yields: 764 The flattened elements. 765 """ 766 for i in nested_list: 767 if isinstance(i, (list, tuple)): 768 yield from flatten2(i) 769 else: 770 yield i 771 772 773def round2(n: float, cutoff: int = 25) -> int: 774 """Round a number to the nearest multiple of cutoff. 775 776 Args: 777 n: The number to round. 778 cutoff: The cutoff value. 779 780 Returns: 781 The rounded number. 782 """ 783 return cutoff * round(n / cutoff) 784 785 786def is_nested_sequence(value): 787 """Check if a value is a nested sequence. 788 789 Args: 790 value: The value to check. 791 792 Returns: 793 True if the value is a nested sequence, False otherwise. 794 """ 795 if not isinstance(value, (list, tuple, ndarray)): 796 return False # Not a sequence 797 798 for item in value: 799 if not isinstance(item, (list, tuple, ndarray)): 800 return False # At least one element is not a sequence 801 802 return True # All elements are sequences 803 804 805def group_into_bins(values, delta): 806 """Group values into bins. 807 808 Args: 809 values: A list of numbers. 810 delta: The bin size. 811 812 Returns: 813 A list of bins. 814 """ 815 values.sort() 816 bins = [] 817 bin_ = [values[0]] 818 for value in values[1:]: 819 if value[0] - bin_[0][0] <= delta: 820 bin_.append(value) 821 else: 822 bins.append(bin_) 823 bin_ = [value] 824 bins.append(bin_) 825 return bins 826 827 828def equal_cycles( 829 cycle1: list[float], cycle2: list[float], rtol=None, atol=None 830) -> bool: 831 """Check if two cycles are circularly equal. 832 833 Args: 834 cycle1: The first cycle. 835 cycle2: The second cycle. 836 rtol: The relative tolerance. 837 atol: The absolute tolerance. 838 839 Returns: 840 True if the cycles are circularly equal, False otherwise. 841 """ 842 rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol]) 843 844 def check_cycles(cyc1, cyc2, rtol=defaults["rtol"]): 845 for i, val in enumerate(cyc1): 846 if not isclose(val, cyc2[i], rtol=rtol, atol=atol): 847 return False 848 return True 849 850 len_cycle1 = len(cycle1) 851 len_cycle2 = len(cycle2) 852 if len_cycle1 != len_cycle2: 853 return False 854 cycle1 = cycle1[:] 855 cycle1.extend(cycle1) 856 for i in range(len_cycle1): 857 if check_cycles(cycle2, cycle1[i : i + len_cycle2], rtol): 858 return True 859 860 return False 861 862 863def map_ranges( 864 value: float, 865 range1_min: float, 866 range1_max: float, 867 range2_min: float, 868 range2_max: float, 869) -> float: 870 """Map a value from one range to another. 871 872 Args: 873 value: The value to map. 874 range1_min: The minimum of the first range. 875 range1_max: The maximum of the first range. 876 range2_min: The minimum of the second range. 877 range2_max: The maximum of the second range. 878 879 Returns: 880 The mapped value. 881 """ 882 delta1 = range1_max - range1_min 883 delta2 = range2_max - range2_min 884 return (value - range1_min) / delta1 * delta2 + range2_min 885 886 887def binomial(n, k): 888 """Calculate the binomial coefficient. 889 890 Args: 891 n: The number of trials. 892 k: The number of successes. 893 894 Returns: 895 The binomial coefficient. 896 """ 897 if k == 0: 898 res = 1 899 else: 900 res = factorial(n) / (factorial(k) * factorial(n - k)) 901 return res 902 903 904def catalan(n): 905 """Calculate the nth Catalan number. 906 907 Args: 908 n: The index of the Catalan number. 909 910 Returns: 911 The nth Catalan number. 912 """ 913 if n <= 1: 914 res = 1 915 else: 916 res = factorial(2 * n) / (factorial(n + 1) * factorial(n)) 917 return res 918 919 920def reg_poly_points(pos: Point, n: int, r: float) -> Sequence[Point]: 921 """Return a regular polygon points list with n sides, r radius, and pos center. 922 923 Args: 924 pos: The center position of the polygon. 925 n: The number of sides. 926 r: The radius. 927 928 Returns: 929 A sequence of points representing the polygon. 930 """ 931 angle = 2 * pi / n 932 x, y = pos[:2] 933 points = [[cos(angle * i) * r + x, sin(angle * i) * r + y] for i in range(n)] 934 points.append(points[0]) 935 return points 936 937 938def solve_quadratic_eq(a, b, c, tolerance=1e-5): 939 """Solves a quadratic equation of the form ax^2 + bx + c = 0.""" 940 941 discr = b**2 - (4 * a * c) # discriminant 942 943 if discr < 0: 944 res = [] 945 elif isclose(discr, 0, rtol=0, atol=tolerance): 946 # one solution 947 res = [(-b + discr) / (2 * a)] 948 else: 949 a2 = a * 2 950 sqrt_discr = sqrt(discr) 951 x1 = (-b + sqrt_discr) / a2 952 x2 = (-b - sqrt_discr) / a2 953 res = [x1, x2] 954 955 return res 956 957 958def solve_quartic_eq(a: float, b: float, c: float, d: float, e: float) -> list[float]: 959 """ 960 Solves a quartic equation of the form ax^4 + bx^3 + cx^2 + dx + e = 0. 961 962 Args: 963 a: The coefficient of x^4. 964 b: The coefficient of x^3. 965 c: The coefficient of x^2. 966 d: The coefficient of x. 967 e: The constant term. 968 969 Returns: 970 A numpy array containing the four roots of the equation. 971 """ 972 973 return np.roots((a, b, c, d, e)).tolist() 974 975 976def solve_complex_quadratic_eq( 977 a: complex, b: complex, c: complex, tolerance: float = 1e-5 978) -> list[complex]: 979 """Solves a quadratic equation of the form ax^2 + bx + c = 0, 980 where a, b, and c can be complex numbers. 981 982 Args: 983 a: The complex coefficient of x^2. 984 b: The complex coefficient of x. 985 c: The complex constant term. 986 tolerance: The tolerance for floating-point comparisons. 987 """ 988 discr = (b**2) - 4 * (a * c) # discriminant 989 a2 = a * 2 990 if isclose(discr, 0, rtol=0, atol=tolerance): 991 x1 = -b / a2 992 res = [x1] 993 elif discr > 0: 994 sqrt_discr = sqrt(discr) 995 x1 = (-b - sqrt_discr) / a2 996 x2 = (-b + sqrt_discr) / a2 997 res = [x1, x2] 998 else: 999 sqrt_discr = cmath.sqrt(discr) 1000 x1 = (-b - sqrt_discr) / a2 1001 x2 = (-b + sqrt_discr) / a2 1002 res = [x1, x2] 1003 1004 return res
26def time_it(func): 27 """Decorator to time a function""" 28 29 @wraps(func) 30 def time_it_wrapper(*args, **kwargs): 31 start_time = perf_counter() 32 result = func(*args, **kwargs) 33 end_time = perf_counter() 34 total_time = end_time - start_time 35 print(f"Function {func.__name__} Took {total_time:.6f} seconds") 36 return result 37 38 return time_it_wrapper
Decorator to time a function
41def close_logger(logger): 42 """Close the logger and remove all handlers. 43 44 Args: 45 logger: The logger instance to close. 46 """ 47 for handler in logger.handlers: 48 handler.close() 49 logger.removeHandler(handler)
Close the logger and remove all handlers.
Arguments:
- logger: The logger instance to close.
52def get_file_path_with_rev(directory, script_path, ext=".pdf"): 53 """Get the file path with a revision number. 54 55 Args: 56 directory: The directory to search for files. 57 script_path: The script file path. 58 ext: The file extension. 59 60 Returns: 61 The file path with a revision number. 62 """ 63 64 # Get the file path of the script 65 def get_rev_number(file_name): 66 match = re.search(r"_\d+$", file_name) 67 if match: 68 rev = match.group()[1:] # remove the underscore 69 if rev is not None: 70 return int(rev) 71 return 0 72 73 # script_path = __file__ 74 filename = os.path.basename(script_path) 75 filename, _ = os.path.splitext(filename) 76 # check if the file is in the current directory 77 files = os.listdir(directory) 78 file_names = [ 79 os.path.splitext(item)[0] 80 for item in files 81 if os.path.isfile(os.path.join(directory, item)) 82 ] 83 existing = [item for item in file_names if item.startswith(filename)] 84 if not existing: 85 return os.path.join(directory, filename + ext) 86 87 revs = [get_rev_number(file) for file in existing] 88 if revs is None: 89 rev = 1 90 else: 91 rev = max(revs) + 1 92 93 return os.path.join(directory, f"{filename}_{rev}" + ext)
Get the file path with a revision number.
Arguments:
- directory: The directory to search for files.
- script_path: The script file path.
- ext: The file extension.
Returns:
The file path with a revision number.
96def remove_file_handler(logger, handler): 97 """Remove a handler from a logger. 98 99 Args: 100 logger: The logger instance. 101 handler: The handler to remove. 102 """ 103 logger.removeHandler(handler) 104 handler.close()
Remove a handler from a logger.
Arguments:
- logger: The logger instance.
- handler: The handler to remove.
107def pretty_print_coords(coords: Sequence[Point]) -> str: 108 """Print the coordinates with a precision of 2. 109 110 Args: 111 coords: A sequence of Point objects. 112 113 Returns: 114 A string representation of the coordinates. 115 """ 116 return ( 117 "(" + ", ".join([f"({coord[0]:.2f}, {coord[1]:.2f})" for coord in coords]) + ")" 118 )
Print the coordinates with a precision of 2.
Arguments:
- coords: A sequence of Point objects.
Returns:
A string representation of the coordinates.
121def is_file_empty(file_path): 122 """Check if a file is empty. 123 124 Args: 125 file_path: The path to the file. 126 127 Returns: 128 True if the file is empty, False otherwise. 129 """ 130 return os.path.getsize(file_path) == 0
Check if a file is empty.
Arguments:
- file_path: The path to the file.
Returns:
True if the file is empty, False otherwise.
133def wait_for_file_availability(file_path, timeout=None, check_interval=1): 134 """Check if a file is available for writing. 135 136 Args: 137 file_path: The path to the file. 138 timeout: The timeout period in seconds. 139 check_interval: The interval to check the file availability. 140 141 Returns: 142 True if the file is available, False otherwise. 143 """ 144 start_time = monotonic() 145 while True: 146 try: 147 # Attempt to open the file in write mode. This will raise an exception 148 # if the file is currently locked or being written to. 149 with open(file_path, "a", encoding="utf-8"): 150 # If the file was successfully opened, it's available. 151 return True 152 except IOError: 153 # The file is likely in use. 154 if timeout is not None and (monotonic() - start_time) > timeout: 155 # Timeout period elapsed. 156 return False # Or raise a TimeoutError if you prefer 157 time.sleep(check_interval) 158 except Exception as e: 159 # Handle other potential exceptions (e.g., file not found) as needed 160 print(f"An error occurred: {e}") 161 return False
Check if a file is available for writing.
Arguments:
- file_path: The path to the file.
- timeout: The timeout period in seconds.
- check_interval: The interval to check the file availability.
Returns:
True if the file is available, False otherwise.
164def detokenize(text: str) -> str: 165 """Replace the special Latex characters with their Latex commands. 166 167 Args: 168 text: The text to detokenize. 169 170 Returns: 171 The detokenized text. 172 """ 173 if text.startswith("$") and text.endswith("$"): 174 res = text 175 else: 176 replacements = { 177 "\\": r"\textbackslash ", 178 "{": r"\{", 179 "}": r"\}", 180 "$": r"\$", 181 "&": r"\&", 182 "%": r"\%", 183 "#": r"\#", 184 "_": r"\_", 185 "^": r"\^{}", 186 "~": r"\textasciitilde{}", 187 } 188 for char, replacement in replacements.items(): 189 text = text.replace(char, replacement) 190 res = text 191 192 return res
Replace the special Latex characters with their Latex commands.
Arguments:
- text: The text to detokenize.
Returns:
The detokenized text.
195def get_text_dimensions(text, font_path, font_size): 196 """Return the width and height of the text. 197 198 Args: 199 text: The text to measure. 200 font_path: The path to the font file. 201 font_size: The size of the font. 202 203 Returns: 204 A tuple containing the width and height of the text. 205 """ 206 font = ImageFont.truetype(font_path, font_size) 207 _, descent = font.getmetrics() 208 text_width = font.getmask(text).getbbox()[2] 209 text_height = font.getmask(text).getbbox()[3] + descent 210 return text_width, text_height
Return the width and height of the text.
Arguments:
- text: The text to measure.
- font_path: The path to the font file.
- font_size: The size of the font.
Returns:
A tuple containing the width and height of the text.
213def timing(func): 214 """Print the execution time of a function. 215 216 Args: 217 func: The function to time. 218 219 Returns: 220 The wrapped function. 221 """ 222 223 @wraps(func) 224 def wrap(*args, **kw): 225 start_time = time() 226 result = func(*args, **kw) 227 end_time = time() 228 elapsed_time = end_time - start_time 229 print(f"function:{func.__name__} took: {elapsed_time:.4f} sec") 230 231 return result 232 233 return wrap
Print the execution time of a function.
Arguments:
- func: The function to time.
Returns:
The wrapped function.
236def find_nearest_value(values: array, value: float) -> float: 237 """Find the closest value in an array to a given number. 238 239 Args: 240 values: A NumPy array. 241 value: The number to find the closest value to. 242 243 Returns: 244 The closest value in the array to the given number. 245 """ 246 arr = np.asarray(values) 247 idx = (np.abs(arr - value)).argmin() 248 249 return arr[idx]
Find the closest value in an array to a given number.
Arguments:
- values: A NumPy array.
- value: The number to find the closest value to.
Returns:
The closest value in the array to the given number.
252def nested_count(nested_sequence): 253 """Return the total number of items in a nested sequence. 254 255 Args: 256 nested_sequence: A nested sequence. 257 258 Returns: 259 The total number of items in the nested sequence. 260 """ 261 return sum( 262 nested_count(item) if isinstance(item, (list, tuple, ndarray)) else 1 263 for item in nested_sequence 264 )
Return the total number of items in a nested sequence.
Arguments:
- nested_sequence: A nested sequence.
Returns:
The total number of items in the nested sequence.
267def decompose_transformations(transformation_matrix): 268 """Decompose a 3x3 transformation matrix into translation, rotation, and scale components. 269 270 Args: 271 transformation_matrix: A 3x3 transformation matrix. 272 273 Returns: 274 A tuple containing the translation, rotation, and scale components. 275 """ 276 xform = transformation_matrix 277 translation = xform[2, :2] 278 rotation = np.arctan2(xform[0, 1], xform[0, 0]) 279 scale = np.linalg.norm(xform[:2, 0]), np.linalg.norm(xform[:2, 1]) 280 281 return translation, rotation, scale
Decompose a 3x3 transformation matrix into translation, rotation, and scale components.
Arguments:
- transformation_matrix: A 3x3 transformation matrix.
Returns:
A tuple containing the translation, rotation, and scale components.
284def check_directory(dir_path): 285 """Check if a directory is valid and writable. 286 287 Args: 288 dir_path: The path to the directory. 289 290 Returns: 291 A tuple containing a boolean indicating validity and an error message. 292 """ 293 error_msg = [] 294 295 def dir_exists(): 296 nonlocal error_msg 297 parent_dir = os.path.dirname(dir_path) 298 if not os.path.exists(parent_dir): 299 error_msg.append("Error! Parent directory doesn't exist") 300 301 def is_writable(): 302 nonlocal error_msg 303 parent_dir = os.path.dirname(dir_path) 304 if not os.access(parent_dir, os.W_OK): 305 error_msg.append("Error! Path is not writable.") 306 307 dir_exists() 308 is_writable() 309 if error_msg: 310 res = False, "\n".join(error_msg) 311 else: 312 res = True, "" 313 314 return res
Check if a directory is valid and writable.
Arguments:
- dir_path: The path to the directory.
Returns:
A tuple containing a boolean indicating validity and an error message.
317def analyze_path(file_path, overwrite): 318 """Check if a file path is valid and writable. 319 320 Args: 321 file_path: The path to the file. 322 overwrite: Whether to overwrite the file if it exists. 323 324 Returns: 325 A tuple containing a boolean indicating validity, the file extension, and an error message. 326 """ 327 supported_types = (".pdf", ".svg", ".ps", ".eps", ".tex") 328 error_msg = "" 329 330 def is_writable(): 331 nonlocal error_msg 332 parent_dir = os.path.dirname(file_path) 333 if os.access(parent_dir, os.W_OK): 334 res = True 335 else: 336 error_msg = "Error! Path is not writable." 337 res = False 338 339 return res 340 341 def is_supported(): 342 nonlocal error_msg 343 extension = Path(file_path).suffix 344 if extension in supported_types: 345 res = True 346 else: 347 error_msg = f"Error! Only {', '.join(supported_types)} supported." 348 res = False 349 350 return res 351 352 def can_overwrite(overwrite): 353 nonlocal error_msg 354 if os.path.exists(file_path): 355 if overwrite is None: 356 overwrite = defaults["overwrite_files"] 357 if overwrite: 358 res = True 359 else: 360 error_msg = ( 361 "Error! File exists. Use canvas." 362 "save(f_path, overwrite=True) to overwrite." 363 ) 364 res = False 365 else: 366 res = True 367 368 return res 369 370 try: 371 file_path = os.path.abspath(file_path) 372 if is_writable() and is_supported() and can_overwrite(overwrite): 373 res = (True, "", Path(file_path).suffix) 374 else: 375 res = (False, error_msg, "") 376 377 return res 378 except ( 379 Exception 380 ) as e: # Million other ways a file path is not valid but life is short! 381 return False, f"Path Error! {e}", ""
Check if a file path is valid and writable.
Arguments:
- file_path: The path to the file.
- overwrite: Whether to overwrite the file if it exists.
Returns:
A tuple containing a boolean indicating validity, the file extension, and an error message.
384def can_be_xform_matrix(seq): 385 """Check if a sequence can be converted to a transformation matrix. 386 387 Args: 388 seq: The sequence to check. 389 390 Returns: 391 True if the sequence can be converted to a transformation matrix, False otherwise. 392 """ 393 # check if it is a sequence that can be 394 # converted to a transformation matrix 395 try: 396 arr = array(seq) 397 return is_xform_matrix(arr) 398 except Exception: 399 return False
Check if a sequence can be converted to a transformation matrix.
Arguments:
- seq: The sequence to check.
Returns:
True if the sequence can be converted to a transformation matrix, False otherwise.
402def is_sequence(value): 403 """Check if a value is a sequence. 404 405 Args: 406 value: The value to check. 407 408 Returns: 409 True if the value is a sequence, False otherwise. 410 """ 411 return isinstance(value, (list, tuple, array))
Check if a value is a sequence.
Arguments:
- value: The value to check.
Returns:
True if the value is a sequence, False otherwise.
414def rel_coord(dx: float, dy: float, center: Point) -> Point: 415 """Return the relative coordinates. 416 417 Args: 418 dx: The x-coordinate difference. 419 dy: The y-coordinate difference. 420 center: The center coordinates. 421 422 Returns: 423 The relative coordinates. 424 """ 425 return dx + center[0], dy + center[1]
Return the relative coordinates.
Arguments:
- dx: The x-coordinate difference.
- dy: The y-coordinate difference.
- center: The center coordinates.
Returns:
The relative coordinates.
428def rel_polar(r: float, angle: float, center: Point) -> Point: 429 """Return the coordinates. 430 431 Args: 432 r: The radius. 433 angle: The angle in radians. 434 center: The center coordinates. 435 436 Returns: 437 The coordinates. 438 """ 439 x, y = center[:2] 440 x1 = x + r * cos(angle) 441 y1 = y + r * sin(angle) 442 443 return x1, y1
Return the coordinates.
Arguments:
- r: The radius.
- angle: The angle in radians.
- center: The center coordinates.
Returns:
The coordinates.
414def rel_coord(dx: float, dy: float, center: Point) -> Point: 415 """Return the relative coordinates. 416 417 Args: 418 dx: The x-coordinate difference. 419 dy: The y-coordinate difference. 420 center: The center coordinates. 421 422 Returns: 423 The relative coordinates. 424 """ 425 return dx + center[0], dy + center[1]
Return the relative coordinates.
Arguments:
- dx: The x-coordinate difference.
- dy: The y-coordinate difference.
- center: The center coordinates.
Returns:
The relative coordinates.
428def rel_polar(r: float, angle: float, center: Point) -> Point: 429 """Return the coordinates. 430 431 Args: 432 r: The radius. 433 angle: The angle in radians. 434 center: The center coordinates. 435 436 Returns: 437 The coordinates. 438 """ 439 x, y = center[:2] 440 x1 = x + r * cos(angle) 441 y1 = y + r * sin(angle) 442 443 return x1, y1
Return the coordinates.
Arguments:
- r: The radius.
- angle: The angle in radians.
- center: The center coordinates.
Returns:
The coordinates.
449def axis(angle: float, length: float = 10) -> Line: 450 """Return a line [(x1, y1), (x2, y2)] with the given angle 451 and length. 452 Args: 453 angle: The angle between the line and the x-axis, in radians. 454 length: The length of the line. 455 456 Returns: 457 A line represented as a tuple of two points. 458 """ 459 length2 = length / 2 460 x1 = cos(angle) * length2 461 y1 = sin(angle) * length2 462 x2 = -x1 463 y2 = -y1 464 465 return (x1, y1), (x2, y2)
Return a line [(x1, y1), (x2, y2)] with the given angle and length.
Arguments:
- angle: The angle between the line and the x-axis, in radians.
- length: The length of the line.
Returns:
A line represented as a tuple of two points.
470def flatten(points): 471 """Flatten the points and return it as a list. 472 473 Args: 474 points: A sequence of points. 475 476 Returns: 477 A flattened list of points. 478 """ 479 if isinstance(points, set): 480 points = list(points) 481 if isinstance(points, np.ndarray): 482 flat = list(points[:, :2].flatten()) 483 elif isinstance(points, collections.abc.Sequence): 484 if isinstance(points[0], collections.abc.Sequence): 485 flat = list(reduce(lambda x, y: x + y, [list(pnt[:2]) for pnt in points])) 486 else: 487 flat = list(points) 488 else: 489 raise TypeError("Error! Invalid data type.") 490 491 return flat
Flatten the points and return it as a list.
Arguments:
- points: A sequence of points.
Returns:
A flattened list of points.
494def find_closest_value(a_sorted_list, value): 495 """Return the index of the closest value and the value itself in a sorted list. 496 497 Args: 498 a_sorted_list: A sorted list of values. 499 value: The value to find the closest match for. 500 501 Returns: 502 A tuple containing the closest value and its index. 503 """ 504 ind = bisect_left(a_sorted_list, value) 505 506 if ind == 0: 507 return a_sorted_list[0] 508 509 if ind == len(a_sorted_list): 510 return a_sorted_list[-1] 511 512 left = a_sorted_list[ind - 1] 513 right = a_sorted_list[ind] 514 515 if right - value < value - left: 516 return right, ind 517 else: 518 return left, ind - 1
Return the index of the closest value and the value itself in a sorted list.
Arguments:
- a_sorted_list: A sorted list of values.
- value: The value to find the closest match for.
Returns:
A tuple containing the closest value and its index.
521def value_from_intervals(value, values, intervals): 522 """Return the value from the intervals. 523 Args: 524 value: The value to find. 525 values: The values to search. 526 intervals: The intervals to search. 527 Returns: 528 The value from the intervals. 529 """ 530 531 return values[bisect_left(intervals, value)]
Return the value from the intervals.
Arguments:
- value: The value to find.
- values: The values to search.
- intervals: The intervals to search.
Returns:
The value from the intervals.
534def get_transform(transform): 535 """Return the transformation matrix. 536 537 Args: 538 transform: The transformation matrix or sequence. 539 540 Returns: 541 The transformation matrix. 542 """ 543 if transform is None: 544 # return identity 545 res = array([[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]]) 546 else: 547 if is_xform_matrix(transform): 548 res = transform 549 elif can_be_xform_matrix(transform): 550 res = array(transform) 551 else: 552 raise RuntimeError("Invalid transformation matrix!") 553 return res
Return the transformation matrix.
Arguments:
- transform: The transformation matrix or sequence.
Returns:
The transformation matrix.
556def is_numeric_numpy_array(array_): 557 """Check if it is an array of numbers. 558 559 Args: 560 array_: The array to check. 561 562 Returns: 563 True if the array is numeric, False otherwise. 564 """ 565 if not isinstance(array_, np.ndarray): 566 return False 567 568 numeric_types = { 569 "u", # unsigned integer 570 "i", # signed integer 571 "f", # floating-point 572 "c", 573 } # complex number 574 try: 575 return array_.dtype.kind in numeric_types 576 except AttributeError: 577 return False
Check if it is an array of numbers.
Arguments:
- array_: The array to check.
Returns:
True if the array is numeric, False otherwise.
580def is_xform_matrix(matrix): 581 """Check if it is a 3x3 transformation matrix. 582 583 Args: 584 matrix: The matrix to check. 585 586 Returns: 587 True if the matrix is a 3x3 transformation matrix, False otherwise. 588 """ 589 return ( 590 is_numeric_numpy_array(matrix) and matrix.shape == (3, 3) and matrix.size == 9 591 )
Check if it is a 3x3 transformation matrix.
Arguments:
- matrix: The matrix to check.
Returns:
True if the matrix is a 3x3 transformation matrix, False otherwise.
594def prime_factors(n): 595 """Prime factorization. 596 597 Args: 598 n: The number to factorize. 599 600 Returns: 601 A list of prime factors. 602 """ 603 p = 2 604 factors = [] 605 while n > 1: 606 if n % p: 607 p += 1 608 else: 609 factors.append(p) 610 n = n / p 611 return factors
Prime factorization.
Arguments:
- n: The number to factorize.
Returns:
A list of prime factors.
614def random_id(): 615 """Generate a random ID. 616 617 Returns: 618 A random ID string. 619 """ 620 return base64.b64encode(os.urandom(6)).decode("ascii")
Generate a random ID.
Returns:
A random ID string.
623def decompose_svg_transform(transform): 624 """Decompose a SVG transformation string. 625 626 Args: 627 transform: The SVG transformation string. 628 629 Returns: 630 A tuple containing the decomposed transformation components. 631 """ 632 a, b, c, d, e, f = transform 633 # [[a, c, e], 634 # [b, d, f], 635 # [0, 0, 1]] 636 dx = e 637 dy = f 638 639 sx = np.sign(a) * sqrt(a**2 + c**2) 640 sy = np.sign(d) * sqrt(b**2 + d**2) 641 642 angle = atan2(b, d) 643 644 return dx, dy, sx, sy, angle
Decompose a SVG transformation string.
Arguments:
- transform: The SVG transformation string.
Returns:
A tuple containing the decomposed transformation components.
647def abcdef_svg(transform_matrix): 648 """Return the a, b, c, d, e, f for SVG transformations. 649 650 Args: 651 transform_matrix: A Numpy array representing the transformation matrix. 652 653 Returns: 654 A tuple containing the a, b, c, d, e, f components. 655 """ 656 # [[a, c, e], 657 # [b, d, f], 658 # [0, 0, 1]] 659 a, b, _, c, d, _, e, f, _ = list(transform_matrix.flat) 660 return (a, b, c, d, e, f)
Return the a, b, c, d, e, f for SVG transformations.
Arguments:
- transform_matrix: A Numpy array representing the transformation matrix.
Returns:
A tuple containing the a, b, c, d, e, f components.
663def abcdef_pil(xform_matrix): 664 """Return the a, b, c, d, e, f for PIL transformations. 665 666 Args: 667 xform_matrix: A Numpy array representing the transformation matrix. 668 669 Returns: 670 A tuple containing the a, b, c, d, e, f components. 671 """ 672 a, d, _, b, e, _, c, f, _ = list(xform_matrix.flat) 673 return (a, b, c, d, e, f)
Return the a, b, c, d, e, f for PIL transformations.
Arguments:
- xform_matrix: A Numpy array representing the transformation matrix.
Returns:
A tuple containing the a, b, c, d, e, f components.
676def abcdef_reportlab(xform_matrix): 677 """Return the a, b, c, d, e, f for Reportlab transformations. 678 679 Args: 680 xform_matrix: A Numpy array representing the transformation matrix. 681 682 Returns: 683 A tuple containing the a, b, c, d, e, f components. 684 """ 685 # a, b, _, c, d, _, e, f, _ = list(np.transpose(xform_matrix).flat) 686 a, b, _, c, d, _, e, f, _ = list(xform_matrix.flat) 687 return (a, b, c, d, e, f)
Return the a, b, c, d, e, f for Reportlab transformations.
Arguments:
- xform_matrix: A Numpy array representing the transformation matrix.
Returns:
A tuple containing the a, b, c, d, e, f components.
690def lerp(start, end, t): 691 """Linear interpolation of two values. 692 693 Args: 694 start: The start value. 695 end: The end value. 696 t: The interpolation factor (0 <= t <= 1). 697 698 Returns: 699 The interpolated value. 700 """ 701 return start + t * (end - start)
Linear interpolation of two values.
Arguments:
- start: The start value.
- end: The end value.
- t: The interpolation factor (0 <= t <= 1).
Returns:
The interpolated value.
704def inv_lerp(start, end, value): 705 """Inverse linear interpolation of two values. 706 707 Args: 708 start: The start value. 709 end: The end value. 710 value: The value to interpolate. 711 712 Returns: 713 The interpolation factor (0 <= t <= 1). 714 """ 715 return (value - start) / (end - start)
Inverse linear interpolation of two values.
Arguments:
- start: The start value.
- end: The end value.
- value: The value to interpolate.
Returns:
The interpolation factor (0 <= t <= 1).
718def sanitize_weighted_graph_edges(edges): 719 """Sanitize weighted graph edges. 720 721 Args: 722 edges: A list of weighted graph edges. 723 724 Returns: 725 A sanitized list of weighted graph edges. 726 """ 727 clean_edges = [] 728 s_seen = set() 729 for edge in edges: 730 e1, e2, _ = edge 731 frozen_edge = frozenset((e1, e2)) 732 if frozen_edge in s_seen: 733 continue 734 s_seen.add(frozen_edge) 735 clean_edges.append(edge) 736 clean_edges.sort() 737 return clean_edges
Sanitize weighted graph edges.
Arguments:
- edges: A list of weighted graph edges.
Returns:
A sanitized list of weighted graph edges.
740def sanitize_graph_edges(edges): 741 """Sanitize graph edges. 742 743 Args: 744 edges: A list of graph edges. 745 746 Returns: 747 A sanitized list of graph edges. 748 """ 749 s_edge_set = set() 750 for edge in edges: 751 s_edge_set.add(frozenset(edge)) 752 edges = [tuple(x) for x in s_edge_set] 753 edges = [(min(x), max(x)) for x in edges] 754 edges.sort() 755 return edges
Sanitize graph edges.
Arguments:
- edges: A list of graph edges.
Returns:
A sanitized list of graph edges.
758def flatten2(nested_list): 759 """Flatten a nested list. 760 761 Args: 762 nested_list: The nested list to flatten. 763 764 Yields: 765 The flattened elements. 766 """ 767 for i in nested_list: 768 if isinstance(i, (list, tuple)): 769 yield from flatten2(i) 770 else: 771 yield i
Flatten a nested list.
Arguments:
- nested_list: The nested list to flatten.
Yields:
The flattened elements.
774def round2(n: float, cutoff: int = 25) -> int: 775 """Round a number to the nearest multiple of cutoff. 776 777 Args: 778 n: The number to round. 779 cutoff: The cutoff value. 780 781 Returns: 782 The rounded number. 783 """ 784 return cutoff * round(n / cutoff)
Round a number to the nearest multiple of cutoff.
Arguments:
- n: The number to round.
- cutoff: The cutoff value.
Returns:
The rounded number.
787def is_nested_sequence(value): 788 """Check if a value is a nested sequence. 789 790 Args: 791 value: The value to check. 792 793 Returns: 794 True if the value is a nested sequence, False otherwise. 795 """ 796 if not isinstance(value, (list, tuple, ndarray)): 797 return False # Not a sequence 798 799 for item in value: 800 if not isinstance(item, (list, tuple, ndarray)): 801 return False # At least one element is not a sequence 802 803 return True # All elements are sequences
Check if a value is a nested sequence.
Arguments:
- value: The value to check.
Returns:
True if the value is a nested sequence, False otherwise.
806def group_into_bins(values, delta): 807 """Group values into bins. 808 809 Args: 810 values: A list of numbers. 811 delta: The bin size. 812 813 Returns: 814 A list of bins. 815 """ 816 values.sort() 817 bins = [] 818 bin_ = [values[0]] 819 for value in values[1:]: 820 if value[0] - bin_[0][0] <= delta: 821 bin_.append(value) 822 else: 823 bins.append(bin_) 824 bin_ = [value] 825 bins.append(bin_) 826 return bins
Group values into bins.
Arguments:
- values: A list of numbers.
- delta: The bin size.
Returns:
A list of bins.
829def equal_cycles( 830 cycle1: list[float], cycle2: list[float], rtol=None, atol=None 831) -> bool: 832 """Check if two cycles are circularly equal. 833 834 Args: 835 cycle1: The first cycle. 836 cycle2: The second cycle. 837 rtol: The relative tolerance. 838 atol: The absolute tolerance. 839 840 Returns: 841 True if the cycles are circularly equal, False otherwise. 842 """ 843 rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol]) 844 845 def check_cycles(cyc1, cyc2, rtol=defaults["rtol"]): 846 for i, val in enumerate(cyc1): 847 if not isclose(val, cyc2[i], rtol=rtol, atol=atol): 848 return False 849 return True 850 851 len_cycle1 = len(cycle1) 852 len_cycle2 = len(cycle2) 853 if len_cycle1 != len_cycle2: 854 return False 855 cycle1 = cycle1[:] 856 cycle1.extend(cycle1) 857 for i in range(len_cycle1): 858 if check_cycles(cycle2, cycle1[i : i + len_cycle2], rtol): 859 return True 860 861 return False
Check if two cycles are circularly equal.
Arguments:
- cycle1: The first cycle.
- cycle2: The second cycle.
- rtol: The relative tolerance.
- atol: The absolute tolerance.
Returns:
True if the cycles are circularly equal, False otherwise.
864def map_ranges( 865 value: float, 866 range1_min: float, 867 range1_max: float, 868 range2_min: float, 869 range2_max: float, 870) -> float: 871 """Map a value from one range to another. 872 873 Args: 874 value: The value to map. 875 range1_min: The minimum of the first range. 876 range1_max: The maximum of the first range. 877 range2_min: The minimum of the second range. 878 range2_max: The maximum of the second range. 879 880 Returns: 881 The mapped value. 882 """ 883 delta1 = range1_max - range1_min 884 delta2 = range2_max - range2_min 885 return (value - range1_min) / delta1 * delta2 + range2_min
Map a value from one range to another.
Arguments:
- value: The value to map.
- range1_min: The minimum of the first range.
- range1_max: The maximum of the first range.
- range2_min: The minimum of the second range.
- range2_max: The maximum of the second range.
Returns:
The mapped value.
888def binomial(n, k): 889 """Calculate the binomial coefficient. 890 891 Args: 892 n: The number of trials. 893 k: The number of successes. 894 895 Returns: 896 The binomial coefficient. 897 """ 898 if k == 0: 899 res = 1 900 else: 901 res = factorial(n) / (factorial(k) * factorial(n - k)) 902 return res
Calculate the binomial coefficient.
Arguments:
- n: The number of trials.
- k: The number of successes.
Returns:
The binomial coefficient.
905def catalan(n): 906 """Calculate the nth Catalan number. 907 908 Args: 909 n: The index of the Catalan number. 910 911 Returns: 912 The nth Catalan number. 913 """ 914 if n <= 1: 915 res = 1 916 else: 917 res = factorial(2 * n) / (factorial(n + 1) * factorial(n)) 918 return res
Calculate the nth Catalan number.
Arguments:
- n: The index of the Catalan number.
Returns:
The nth Catalan number.
921def reg_poly_points(pos: Point, n: int, r: float) -> Sequence[Point]: 922 """Return a regular polygon points list with n sides, r radius, and pos center. 923 924 Args: 925 pos: The center position of the polygon. 926 n: The number of sides. 927 r: The radius. 928 929 Returns: 930 A sequence of points representing the polygon. 931 """ 932 angle = 2 * pi / n 933 x, y = pos[:2] 934 points = [[cos(angle * i) * r + x, sin(angle * i) * r + y] for i in range(n)] 935 points.append(points[0]) 936 return points
Return a regular polygon points list with n sides, r radius, and pos center.
Arguments:
- pos: The center position of the polygon.
- n: The number of sides.
- r: The radius.
Returns:
A sequence of points representing the polygon.
939def solve_quadratic_eq(a, b, c, tolerance=1e-5): 940 """Solves a quadratic equation of the form ax^2 + bx + c = 0.""" 941 942 discr = b**2 - (4 * a * c) # discriminant 943 944 if discr < 0: 945 res = [] 946 elif isclose(discr, 0, rtol=0, atol=tolerance): 947 # one solution 948 res = [(-b + discr) / (2 * a)] 949 else: 950 a2 = a * 2 951 sqrt_discr = sqrt(discr) 952 x1 = (-b + sqrt_discr) / a2 953 x2 = (-b - sqrt_discr) / a2 954 res = [x1, x2] 955 956 return res
Solves a quadratic equation of the form ax^2 + bx + c = 0.
959def solve_quartic_eq(a: float, b: float, c: float, d: float, e: float) -> list[float]: 960 """ 961 Solves a quartic equation of the form ax^4 + bx^3 + cx^2 + dx + e = 0. 962 963 Args: 964 a: The coefficient of x^4. 965 b: The coefficient of x^3. 966 c: The coefficient of x^2. 967 d: The coefficient of x. 968 e: The constant term. 969 970 Returns: 971 A numpy array containing the four roots of the equation. 972 """ 973 974 return np.roots((a, b, c, d, e)).tolist()
Solves a quartic equation of the form ax^4 + bx^3 + cx^2 + dx + e = 0.
Arguments:
- a: The coefficient of x^4.
- b: The coefficient of x^3.
- c: The coefficient of x^2.
- d: The coefficient of x.
- e: The constant term.
Returns:
A numpy array containing the four roots of the equation.
977def solve_complex_quadratic_eq( 978 a: complex, b: complex, c: complex, tolerance: float = 1e-5 979) -> list[complex]: 980 """Solves a quadratic equation of the form ax^2 + bx + c = 0, 981 where a, b, and c can be complex numbers. 982 983 Args: 984 a: The complex coefficient of x^2. 985 b: The complex coefficient of x. 986 c: The complex constant term. 987 tolerance: The tolerance for floating-point comparisons. 988 """ 989 discr = (b**2) - 4 * (a * c) # discriminant 990 a2 = a * 2 991 if isclose(discr, 0, rtol=0, atol=tolerance): 992 x1 = -b / a2 993 res = [x1] 994 elif discr > 0: 995 sqrt_discr = sqrt(discr) 996 x1 = (-b - sqrt_discr) / a2 997 x2 = (-b + sqrt_discr) / a2 998 res = [x1, x2] 999 else: 1000 sqrt_discr = cmath.sqrt(discr) 1001 x1 = (-b - sqrt_discr) / a2 1002 x2 = (-b + sqrt_discr) / a2 1003 res = [x1, x2] 1004 1005 return res
Solves a quadratic equation of the form ax^2 + bx + c = 0, where a, b, and c can be complex numbers.
Arguments:
- a: The complex coefficient of x^2.
- b: The complex coefficient of x.
- c: The complex constant term.
- tolerance: The tolerance for floating-point comparisons.