From 69f103731284de107694d900a182f7ae066fe9e4 Mon Sep 17 00:00:00 2001 From: Maximilian Pierzyna Date: Mon, 23 Jan 2023 16:44:54 +0100 Subject: [PATCH 1/5] Added multiprocessing capability to duplicate power removal for faster execution. Also automated PEP8 formatting --- buckinghampy/buckinghampi.py | 164 ++++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 70 deletions(-) diff --git a/buckinghampy/buckinghampi.py b/buckinghampy/buckinghampi.py index d3973fc..4315e14 100644 --- a/buckinghampy/buckinghampi.py +++ b/buckinghampy/buckinghampi.py @@ -10,12 +10,15 @@ __email__ = "karammokbel@gmail.com" __status__ = "Production" +import multiprocessing +from concurrent.futures import ProcessPoolExecutor, Future +from itertools import combinations, permutations, chain + +import numpy as np import sympy as sp -from sympy.parsing.sympy_parser import parse_expr -from sympy.core.mul import Mul, Pow from sympy.core.expr import Expr -import numpy as np -from itertools import combinations,permutations +from sympy.core.mul import Mul, Pow +from sympy.parsing.sympy_parser import parse_expr from tabulate import tabulate try: @@ -23,23 +26,58 @@ except: pass + +def find_duplicates(pi_set, other) -> list: + duplicate = [] + permutations_sets = permutations(pi_set) + for p_set in permutations_sets: + # create a permutation vector from the permutation set + p_V = sp.Matrix(list(p_set)) + # create a vector from the other set of dimensionless groups that we are comparing to. + o_V = sp.Matrix(other) + # create an element wise inverse of the vector of dimensionless groups + o_V_inv = o_V.applyfunc(lambda x: x ** (-1)) + + result = sp.matrix_multiply_elementwise(p_V, o_V) + # obtain the index of numerical value in the result vector. + # numerical values indicates that one dimensionless group is the inverse of the other group + # in this algorithm the numerical value will be equal to 1 (this is a result of the nullspace function in sympy) + idx_num_result = [x for x in range(len(p_set)) if isinstance(result[x, 0], sp.Number)] + # also repeat the multiplication with the inverse vector + result_inv = sp.matrix_multiply_elementwise(p_V, o_V_inv) + # check for the index of the numerical values in the result vector + idx_num_result_inv = [x for x in range(len(p_set)) if isinstance(result_inv[x, 0], sp.Number)] + # concatinate the indices into one list + all_indices = idx_num_result + idx_num_result_inv + # compare if the two vector are duplicates + if set(all_indices) == set(list(range(len(p_set)))): + duplicate.append(pi_set) + + return duplicate + + class BuckinghamPi: - def __init__(self): + def __init__(self, n_jobs: int = 1): ''' Construct an instance of the BuckinghamPi theorem ''' - self.__var_from_idx={} + self.__var_from_idx = {} self.__idx_from_var = {} - self.__variables={} - self.__sym_variables={} - self.__flagged_var = {'var_name':None, 'var_index':None,'selected':False} + self.__variables = {} + self.__sym_variables = {} + self.__flagged_var = {'var_name': None, 'var_index': None, 'selected': False} self.__null_spaces = [] - self.__fundamental_vars_used = [] # list of fundamental variables being used + self.__fundamental_vars_used = [] # list of fundamental variables being used self.__prefixed_dimensionless_terms = [] + if (n_jobs == -1) or (n_jobs is None): + self.n_jobs = multiprocessing.cpu_count() + else: + self.n_jobs = n_jobs + @property def fundamental_variables(self): ''' @@ -54,20 +92,23 @@ def variables(self): ''' return self.__variables - - def __parse_expression(self,string:str): + def __parse_expression(self, string: str): if '^' in string: # convert the xor operator to power operator - string = string.replace('^','**') + string = string.replace('^', '**') expr = parse_expr(string.lower()) - if not (isinstance(expr,Mul) or isinstance(expr,Pow) or isinstance(expr,sp.Symbol)): - raise Exception('expression of type {} is not of the accepted types ({}, {}, {})'.format(type(expr), Mul, Pow, sp.Symbol)) + if not (isinstance(expr, Mul) or isinstance(expr, Pow) or isinstance(expr, sp.Symbol)): + raise Exception( + 'expression of type {} is not of the accepted types ({}, {}, {})'.format(type(expr), Mul, Pow, + sp.Symbol)) if expr.as_coeff_Mul()[0] != 1: - raise Exception('cannot have coefficients, {}, that multiply the expression {}'.format(expr.as_coeff_Mul()[0],expr.as_coeff_Mul()[1])) + raise Exception( + 'cannot have coefficients, {}, that multiply the expression {}'.format(expr.as_coeff_Mul()[0], + expr.as_coeff_Mul()[1])) - #extract the physical dimensions from the dimensions expressions + # extract the physical dimensions from the dimensions expressions used_symbols = list(expr.free_symbols) for sym in used_symbols: if not sym in self.__fundamental_vars_used: @@ -75,7 +116,7 @@ def __parse_expression(self,string:str): return expr - def __extract_exponents(self,expr:Expr): + def __extract_exponents(self, expr: Expr): num_physical_dimensions = len(self.__fundamental_vars_used) vect = np.zeros(num_physical_dimensions) args = list(expr.args) if list(expr.args) else [expr] @@ -85,10 +126,10 @@ def __extract_exponents(self,expr:Expr): else: for e in args: if isinstance(expr, sp.Symbol): - vect[self.__fundamental_vars_used.index(e)]= int(1) + vect[self.__fundamental_vars_used.index(e)] = int(1) # print('({}, {})'.format(e, 1)) else: - var, exponent= e.as_base_exp() + var, exponent = e.as_base_exp() vect[self.__fundamental_vars_used.index(var)] = int(exponent) # print('({}, {})'.format(var, exponent)) @@ -102,11 +143,11 @@ def add_variable(self, name: str, dimensions: str, non_repeating=False): :param non_repeating: (boolean) select a variable to belong to the non-repeating variables matrix. This will ensure that the selected variable only shows up in one dimensionless group. ''' - if dimensions!="1": - expr = self.__parse_expression(dimensions) - self.__variables.update({name:expr}) - var_idx = len(list(self.__variables.keys()))-1 - self.__var_from_idx[var_idx]= name + if dimensions != "1": + expr = self.__parse_expression(dimensions) + self.__variables.update({name: expr}) + var_idx = len(list(self.__variables.keys())) - 1 + self.__var_from_idx[var_idx] = name self.__idx_from_var[name] = var_idx if non_repeating and (self.__flagged_var['selected'] == False): self.__flagged_var['var_name'] = name @@ -138,7 +179,7 @@ def __create_symbolic_variables(self): self.__sym_variables[var_name] = sp.symbols(var_name) def __solve_null_spaces(self): - if self.__flagged_var['selected']==True: + if self.__flagged_var['selected'] == True: self.__solve_null_spaces_for_flagged_variables() else: @@ -151,7 +192,7 @@ def __solve_null_spaces(self): def __solve_null_spaces_for_flagged_variables(self): - assert self.__flagged_var['selected']==True, " you need to select a variable to be explicit" + assert self.__flagged_var['selected'] == True, " you need to select a variable to be explicit" n = self.num_variable m = len(self.__fundamental_vars_used) @@ -162,30 +203,30 @@ def __solve_null_spaces_for_flagged_variables(self): del all_idx[self.__flagged_var['var_index']] # print(all_idx) - all_combs = list(combinations(all_idx,m)) + all_combs = list(combinations(all_idx, m)) # print(all_combs) num_det_0 = 0 for comb in all_combs: temp_comb = list(comb).copy() - extra_vars = [i for i in original_indicies if i not in temp_comb ] + extra_vars = [i for i in original_indicies if i not in temp_comb] b_ns = [] for extra_var in extra_vars: new_order = {} temp_comb.append(extra_var) - A = self.M[:,temp_comb].copy() - for num,var_idx in enumerate(temp_comb): - new_order[num] = self.__var_from_idx[var_idx] + A = self.M[:, temp_comb].copy() + for num, var_idx in enumerate(temp_comb): + new_order[num] = self.__var_from_idx[var_idx] B = sp.Matrix(A) - test_mat = B[:,:m] - if sp.det(test_mat) !=0: + test_mat = B[:, :m] + if sp.det(test_mat) != 0: ns = B.nullspace()[0] b_ns.append({'order': new_order, 'power': ns.tolist()}) else: - num_det_0+=1 + num_det_0 += 1 temp_comb = list(comb).copy() - if b_ns: # if b_ns is not empty add it to the nullspaces list + if b_ns: # if b_ns is not empty add it to the nullspaces list self.__null_spaces.append(b_ns) # print("num of det 0 : ",num_det_0) @@ -196,7 +237,7 @@ def __construct_symbolic_pi_terms(self): for term in space: expr = 1 idx = 0 - for order,power in zip(term['order'].keys(),term['power']): + for order, power in zip(term['order'].keys(), term['power']): expr *= self.__sym_variables[term['order'][order]] ** sp.nsimplify(sp.Rational(power[0])) idx += 1 spacepiterms.append(expr) @@ -213,34 +254,18 @@ def __rm_duplicated_powers(self): # this algorithm rely on the fact that the nullspace function # in sympy set one free variable to 1 and the all other to zero # then solve the system by back substitution. - duplicate = [] + duplicate_futures: list[Future] = [] dummy_other_terms = self.__allpiterms.copy() - for num_set, pi_set in enumerate(self.__allpiterms): - dummy_other_terms.remove(pi_set) - for num_other, other in enumerate(dummy_other_terms): - permutations_sets = permutations(pi_set) - for p_set in permutations_sets: - # create a permutation vector from the permutation set - p_V = sp.Matrix(list(p_set)) - # create a vector from the other set of dimensionless groups that we are comparing to. - o_V = sp.Matrix(other) - # create an element wise inverse of the vector of dimensionless groups - o_V_inv = o_V.applyfunc(lambda x:x**(-1)) - - result = sp.matrix_multiply_elementwise(p_V, o_V) - # obtain the index of numerical value in the result vector. - # numerical values indicates that one dimensionless group is the inverse of the other group - # in this algorithm the numerical value will be equal to 1 (this is a result of the nullspace function in sympy) - idx_num_result = [x for x in range(len(p_set)) if isinstance(result[x,0],sp.Number)] - # also repeat the multiplication with the inverse vector - result_inv = sp.matrix_multiply_elementwise(p_V, o_V_inv) - # check for the index of the numerical values in the result vector - idx_num_result_inv = [x for x in range(len(p_set)) if isinstance(result_inv[x,0],sp.Number)] - # concatinate the indices into one list - all_indices = idx_num_result + idx_num_result_inv - # compare if the two vector are duplicates - if set(all_indices) == set(list(range(len(p_set)))): - duplicate.append(pi_set) + + with ProcessPoolExecutor(max_workers=self.n_jobs) as e: + for num_set, pi_set in enumerate(self.__allpiterms): + dummy_other_terms.remove(pi_set) + for num_other, other in enumerate(dummy_other_terms): + future = e.submit(find_duplicates, pi_set=pi_set, other=other) + duplicate_futures.append(future) + + # Gather results from parallel execution + duplicate = chain.from_iterable((f.result() for f in duplicate_futures)) # remove duplicates from the main dict of all pi terms for dup in duplicate: @@ -276,19 +301,18 @@ def pi_terms(self): ''' return self.__allpiterms - def __Jupyter_print(self): ''' print the rendered Latex format in Jupyter cell''' for set_num, space in enumerate(self.__allpiterms): - latex_str= '\\text{Set }' - latex_str+='{}: \\quad'.format(set_num+1) + latex_str = '\\text{Set }' + latex_str += '{}: \\quad'.format(set_num + 1) for num, term in enumerate(space): - latex_str += '\\pi_{} = '.format(num+1)+sp.latex(term) + latex_str += '\\pi_{} = '.format(num + 1) + sp.latex(term) latex_str += '\\quad' display(Math(latex_str)) display(Markdown('---')) - def __tabulate_print(self,latex_string=False): + def __tabulate_print(self, latex_string=False): ''' print the dimensionless sets in a tabulated format''' latex_form = [] @@ -327,4 +351,4 @@ def print_all(self, latex_string=False): self.__Jupyter_print() except: ''' print the dimensionless sets in a tabulated format when in terminal session''' - self.__tabulate_print(latex_string) \ No newline at end of file + self.__tabulate_print(latex_string) From fef98bc818a23495e9301310aadac5e1fc62bb9e Mon Sep 17 00:00:00 2001 From: Maximilian Pierzyna Date: Wed, 19 Feb 2025 22:26:02 +0100 Subject: [PATCH 2/5] Add logging. Add chunking to multiprocessing execution --- buckinghampy/buckinghampi.py | 39 +++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/buckinghampy/buckinghampi.py b/buckinghampy/buckinghampi.py index 4315e14..4466818 100644 --- a/buckinghampy/buckinghampi.py +++ b/buckinghampy/buckinghampi.py @@ -10,9 +10,10 @@ __email__ = "karammokbel@gmail.com" __status__ = "Production" +import concurrent.futures +import logging import multiprocessing -from concurrent.futures import ProcessPoolExecutor, Future -from itertools import combinations, permutations, chain +from itertools import combinations, permutations import numpy as np import sympy as sp @@ -26,6 +27,8 @@ except: pass +logger = logging.getLogger("buckinghampy") + def find_duplicates(pi_set, other) -> list: duplicate = [] @@ -159,6 +162,7 @@ def add_variable(self, name: str, dimensions: str, non_repeating=False): self.__prefixed_dimensionless_terms.append(sp.symbols(name)) def __create_M(self): + logger.info("Creating M matrix") self.num_variable = len(list(self.__variables.keys())) num_physical_dimensions = len(self.__fundamental_vars_used) if self.num_variable <= num_physical_dimensions: @@ -175,10 +179,12 @@ def __create_M(self): self.M = self.M.transpose() def __create_symbolic_variables(self): + logger.info("Creating symbolic variables") for var_name in self.__variables.keys(): self.__sym_variables[var_name] = sp.symbols(var_name) def __solve_null_spaces(self): + logger.info("Solving null spaces") if self.__flagged_var['selected'] == True: self.__solve_null_spaces_for_flagged_variables() @@ -231,6 +237,7 @@ def __solve_null_spaces_for_flagged_variables(self): # print("num of det 0 : ",num_det_0) def __construct_symbolic_pi_terms(self): + logger.info("Constructing symbolic pi terms") self.__allpiterms = [] for space in self.__null_spaces: spacepiterms = [] @@ -254,18 +261,25 @@ def __rm_duplicated_powers(self): # this algorithm rely on the fact that the nullspace function # in sympy set one free variable to 1 and the all other to zero # then solve the system by back substitution. - duplicate_futures: list[Future] = [] dummy_other_terms = self.__allpiterms.copy() - with ProcessPoolExecutor(max_workers=self.n_jobs) as e: - for num_set, pi_set in enumerate(self.__allpiterms): - dummy_other_terms.remove(pi_set) - for num_other, other in enumerate(dummy_other_terms): - future = e.submit(find_duplicates, pi_set=pi_set, other=other) - duplicate_futures.append(future) - - # Gather results from parallel execution - duplicate = chain.from_iterable((f.result() for f in duplicate_futures)) + # Build list of inputs to be processed + duplicate_inputs = [] + for _, pi_set in enumerate(self.__allpiterms): + dummy_other_terms.remove(pi_set) + for _, other in enumerate(dummy_other_terms): + duplicate_inputs.append((pi_set, other)) + + # Process inputs in parallel + logger.info(f"Removing duplicated powers ({len(duplicate_inputs)} tests)") + with concurrent.futures.ProcessPoolExecutor(max_workers=self.n_jobs) as e: + futures = e.map( + find_duplicates, + *zip(*duplicate_inputs), + chunksize=max(1, len(duplicate_inputs) // self.n_jobs) + ) + # duplicate = list(tqdm.tqdm(futures, total=len(duplicate_inputs))) + duplicate = list(futures) # remove duplicates from the main dict of all pi terms for dup in duplicate: @@ -293,6 +307,7 @@ def generate_pi_terms(self): self.__rm_duplicated_powers() self.__populate_prefixed_dimensionless_groups() + logger.info("Done!") @property def pi_terms(self): From eb8d18cbaa83f5ce9d0025cded29434ee77c95c5 Mon Sep 17 00:00:00 2001 From: Maximilian Pierzyna Date: Wed, 19 Feb 2025 22:26:29 +0100 Subject: [PATCH 3/5] Black formatting --- buckinghampy/buckinghampi.py | 102 ++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/buckinghampy/buckinghampi.py b/buckinghampy/buckinghampi.py index 4466818..21c5a20 100644 --- a/buckinghampy/buckinghampi.py +++ b/buckinghampy/buckinghampi.py @@ -61,14 +61,14 @@ def find_duplicates(pi_set, other) -> list: class BuckinghamPi: def __init__(self, n_jobs: int = 1): - ''' + """ Construct an instance of the BuckinghamPi theorem - ''' + """ self.__var_from_idx = {} self.__idx_from_var = {} self.__variables = {} self.__sym_variables = {} - self.__flagged_var = {'var_name': None, 'var_index': None, 'selected': False} + self.__flagged_var = {"var_name": None, "var_index": None, "selected": False} self.__null_spaces = [] @@ -83,33 +83,37 @@ def __init__(self, n_jobs: int = 1): @property def fundamental_variables(self): - ''' + """ :return: a list of the fundamental variables being used - ''' + """ return self.__fundamental_vars_used @property def variables(self): - ''' + """ :return: a dict of the variables added by the user. - ''' + """ return self.__variables def __parse_expression(self, string: str): - if '^' in string: + if "^" in string: # convert the xor operator to power operator - string = string.replace('^', '**') + string = string.replace("^", "**") expr = parse_expr(string.lower()) if not (isinstance(expr, Mul) or isinstance(expr, Pow) or isinstance(expr, sp.Symbol)): raise Exception( - 'expression of type {} is not of the accepted types ({}, {}, {})'.format(type(expr), Mul, Pow, - sp.Symbol)) + "expression of type {} is not of the accepted types ({}, {}, {})".format( + type(expr), Mul, Pow, sp.Symbol + ) + ) if expr.as_coeff_Mul()[0] != 1: raise Exception( - 'cannot have coefficients, {}, that multiply the expression {}'.format(expr.as_coeff_Mul()[0], - expr.as_coeff_Mul()[1])) + "cannot have coefficients, {}, that multiply the expression {}".format( + expr.as_coeff_Mul()[0], expr.as_coeff_Mul()[1] + ) + ) # extract the physical dimensions from the dimensions expressions used_symbols = list(expr.free_symbols) @@ -139,24 +143,24 @@ def __extract_exponents(self, expr: Expr): return vect def add_variable(self, name: str, dimensions: str, non_repeating=False): - ''' + """ Add variables to use for the pi-theorem :param name: (string) name of the variable to be added :param dimensions: (string) expression of the independent physical variable expressed in terms of the k independent fundamental dimensions. :param non_repeating: (boolean) select a variable to belong to the non-repeating variables matrix. This will ensure that the selected variable only shows up in one dimensionless group. - ''' + """ if dimensions != "1": expr = self.__parse_expression(dimensions) self.__variables.update({name: expr}) var_idx = len(list(self.__variables.keys())) - 1 self.__var_from_idx[var_idx] = name self.__idx_from_var[name] = var_idx - if non_repeating and (self.__flagged_var['selected'] == False): - self.__flagged_var['var_name'] = name - self.__flagged_var['var_index'] = var_idx - self.__flagged_var['selected'] = True - elif non_repeating and (self.__flagged_var['selected'] == True): + if non_repeating and (self.__flagged_var["selected"] == False): + self.__flagged_var["var_name"] = name + self.__flagged_var["var_index"] = var_idx + self.__flagged_var["selected"] = True + elif non_repeating and (self.__flagged_var["selected"] == True): raise Exception("you cannot select more than one variable at a time to be a non_repeating.") else: self.__prefixed_dimensionless_terms.append(sp.symbols(name)) @@ -166,7 +170,7 @@ def __create_M(self): self.num_variable = len(list(self.__variables.keys())) num_physical_dimensions = len(self.__fundamental_vars_used) if self.num_variable <= num_physical_dimensions: - raise Exception('The number of variables has to be greater than the number of physical dimensions.') + raise Exception("The number of variables has to be greater than the number of physical dimensions.") self.M = np.zeros(shape=(self.num_variable, num_physical_dimensions)) # fill M @@ -185,28 +189,28 @@ def __create_symbolic_variables(self): def __solve_null_spaces(self): logger.info("Solving null spaces") - if self.__flagged_var['selected'] == True: + if self.__flagged_var["selected"] == True: self.__solve_null_spaces_for_flagged_variables() else: for idx in self.__var_from_idx.keys(): - self.__flagged_var['var_name'] = self.__var_from_idx[idx] - self.__flagged_var['var_index'] = idx - self.__flagged_var['selected'] = True + self.__flagged_var["var_name"] = self.__var_from_idx[idx] + self.__flagged_var["var_index"] = idx + self.__flagged_var["selected"] = True self.__solve_null_spaces_for_flagged_variables() def __solve_null_spaces_for_flagged_variables(self): - assert self.__flagged_var['selected'] == True, " you need to select a variable to be explicit" + assert self.__flagged_var["selected"] == True, " you need to select a variable to be explicit" n = self.num_variable m = len(self.__fundamental_vars_used) original_indicies = list(range(0, n)) all_idx = original_indicies.copy() - if self.__flagged_var['selected']: - del all_idx[self.__flagged_var['var_index']] + if self.__flagged_var["selected"]: + del all_idx[self.__flagged_var["var_index"]] # print(all_idx) all_combs = list(combinations(all_idx, m)) @@ -227,7 +231,7 @@ def __solve_null_spaces_for_flagged_variables(self): test_mat = B[:, :m] if sp.det(test_mat) != 0: ns = B.nullspace()[0] - b_ns.append({'order': new_order, 'power': ns.tolist()}) + b_ns.append({"order": new_order, "power": ns.tolist()}) else: num_det_0 += 1 @@ -244,8 +248,8 @@ def __construct_symbolic_pi_terms(self): for term in space: expr = 1 idx = 0 - for order, power in zip(term['order'].keys(), term['power']): - expr *= self.__sym_variables[term['order'][order]] ** sp.nsimplify(sp.Rational(power[0])) + for order, power in zip(term["order"].keys(), term["power"]): + expr *= self.__sym_variables[term["order"][order]] ** sp.nsimplify(sp.Rational(power[0])) idx += 1 spacepiterms.append(expr) # check for already existing pi terms in previous null-spaces @@ -276,7 +280,7 @@ def __rm_duplicated_powers(self): futures = e.map( find_duplicates, *zip(*duplicate_inputs), - chunksize=max(1, len(duplicate_inputs) // self.n_jobs) + chunksize=max(1, len(duplicate_inputs) // self.n_jobs), ) # duplicate = list(tqdm.tqdm(futures, total=len(duplicate_inputs))) duplicate = list(futures) @@ -293,9 +297,9 @@ def __populate_prefixed_dimensionless_groups(self): self.__allpiterms[num_set].append(pre_fixed_dimensionless_group) def generate_pi_terms(self): - ''' + """ Generates all the possible pi terms - ''' + """ self.__create_M() self.__create_symbolic_variables() @@ -311,24 +315,24 @@ def generate_pi_terms(self): @property def pi_terms(self): - ''' + """ :return: a list with all the symbolic dimensionless terms for all permutation of the dimensional Matrix M - ''' + """ return self.__allpiterms def __Jupyter_print(self): - ''' print the rendered Latex format in Jupyter cell''' + """print the rendered Latex format in Jupyter cell""" for set_num, space in enumerate(self.__allpiterms): - latex_str = '\\text{Set }' - latex_str += '{}: \\quad'.format(set_num + 1) + latex_str = "\\text{Set }" + latex_str += "{}: \\quad".format(set_num + 1) for num, term in enumerate(space): - latex_str += '\\pi_{} = '.format(num + 1) + sp.latex(term) - latex_str += '\\quad' + latex_str += "\\pi_{} = ".format(num + 1) + sp.latex(term) + latex_str += "\\quad" display(Math(latex_str)) - display(Markdown('---')) + display(Markdown("---")) def __tabulate_print(self, latex_string=False): - ''' print the dimensionless sets in a tabulated format''' + """print the dimensionless sets in a tabulated format""" latex_form = [] for pi_set in self.__allpiterms: @@ -345,9 +349,9 @@ def __tabulate_print(self, latex_string=False): num_of_pi_terms = len(latex_form[0]) - headers = ['sets'] + headers = ["sets"] for num in range(num_of_pi_terms): - headers.append('Pi {}'.format(num + 1)) + headers.append("Pi {}".format(num + 1)) for num, set in enumerate(latex_form): set.insert(0, num + 1) @@ -355,15 +359,15 @@ def __tabulate_print(self, latex_string=False): print(tabulate(latex_form, headers=headers)) def print_all(self, latex_string=False): - ''' + """ print all the sets of dimensionless groups in latex or symbolic form. :latex_string: optional boolean. If set to True the function will print the latex string of the dimensionless groups. if set to False the function will print the symbolic form of the dimensionless groups. - ''' + """ try: - ''' Try to render the latex in Jupyter cell''' + """Try to render the latex in Jupyter cell""" self.__Jupyter_print() except: - ''' print the dimensionless sets in a tabulated format when in terminal session''' + """print the dimensionless sets in a tabulated format when in terminal session""" self.__tabulate_print(latex_string) From 5b7b3ef56deb772831e23498f5075d4d8e65d921 Mon Sep 17 00:00:00 2001 From: Maximilian Pierzyna Date: Tue, 25 Feb 2025 14:32:00 +0100 Subject: [PATCH 4/5] Update multiprocessing again to have progress bar --- buckinghampy/buckinghampi.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/buckinghampy/buckinghampi.py b/buckinghampy/buckinghampi.py index 85c0a53..3a685b5 100644 --- a/buckinghampy/buckinghampi.py +++ b/buckinghampy/buckinghampi.py @@ -19,6 +19,7 @@ import numpy as np import sympy as sp +import tqdm from sympy.core.expr import Expr from sympy.core.mul import Mul, Pow from sympy.parsing.sympy_parser import parse_expr @@ -297,13 +298,15 @@ def __rm_duplicated_powers(self): # Process inputs in parallel logger.info(f"Removing duplicated powers ({len(duplicate_inputs)} tests)") with concurrent.futures.ProcessPoolExecutor(max_workers=self.n_jobs) as e: - futures = e.map( - find_duplicates, - *zip(*duplicate_inputs), - chunksize=max(1, len(duplicate_inputs) // self.n_jobs), - ) - # duplicate = list(tqdm.tqdm(futures, total=len(duplicate_inputs))) - duplicate = list(futures) + futures = [ + e.submit(find_duplicates, pi_set, other) + for pi_set, other in duplicate_inputs + ] + duplicate = [] + for future in tqdm.tqdm( + concurrent.futures.as_completed(futures), total=len(futures) + ): + duplicate.append(future.result()) # remove duplicates from the main dict of all pi terms for dup in duplicate: From edfd8b2f942c67d5600f674e223d880332cdc229 Mon Sep 17 00:00:00 2001 From: Maximilian Pierzyna Date: Fri, 28 Feb 2025 16:14:37 +0100 Subject: [PATCH 5/5] Bugfix to ensure that multi-processing still yields same results. Also check for non-integer dimensions --- buckinghampy/buckinghampi.py | 50 ++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/buckinghampy/buckinghampi.py b/buckinghampy/buckinghampi.py index 3a685b5..3d359fd 100644 --- a/buckinghampy/buckinghampi.py +++ b/buckinghampy/buckinghampi.py @@ -16,6 +16,7 @@ import logging import multiprocessing from itertools import combinations, permutations +from typing import TypeAlias, List import numpy as np import sympy as sp @@ -32,8 +33,13 @@ logger = logging.getLogger("buckinghampy") +PiType: TypeAlias = sp.Expr +PiSetType: TypeAlias = List[PiType] -def find_duplicates(pi_set, other) -> list: + +def find_duplicates_worker( + pi_set: PiSetType, other: List[PiSetType] +) -> List[PiSetType]: duplicate = [] permutations_sets = permutations(pi_set) for p_set in permutations_sets: @@ -128,6 +134,17 @@ def __parse_expression(self, string: str): ) ) + # Make sure dimensions only contain integer exponents + # Technically this only checks for *any* rational number in the expression, + # but units should only be expressions made up of a dimension and an exponent, so implicitly this is fine. + has_non_int_exp = any( + [not a.is_Integer for a in expr.atoms() if not a.is_Symbol] + ) + if has_non_int_exp: + raise ValueError( + f"Dimension {expr} contains non-integer exponent(s), which is not allowed." + ) + # extract the physical dimensions from the dimensions expressions used_symbols = list(expr.free_symbols) for sym in used_symbols: @@ -165,6 +182,7 @@ def add_variable(self, name: str, dimensions: str, non_repeating=False): """ if dimensions != "1": expr = self.__parse_expression(dimensions) + self.__variables.update({name: expr}) var_idx = len(list(self.__variables.keys())) - 1 self.__var_from_idx[var_idx] = name @@ -283,30 +301,28 @@ def __construct_symbolic_pi_terms(self): self.__allpiterms.append(spacepiterms) def __rm_duplicated_powers(self): + logger.info("Removing duplicated pi terms. This can take a looooong time.") # this algorithm rely on the fact that the nullspace function # in sympy set one free variable to 1 and the all other to zero # then solve the system by back substitution. + duplicate = [] dummy_other_terms = self.__allpiterms.copy() - # Build list of inputs to be processed - duplicate_inputs = [] - for _, pi_set in enumerate(self.__allpiterms): - dummy_other_terms.remove(pi_set) - for _, other in enumerate(dummy_other_terms): - duplicate_inputs.append((pi_set, other)) - - # Process inputs in parallel - logger.info(f"Removing duplicated powers ({len(duplicate_inputs)} tests)") - with concurrent.futures.ProcessPoolExecutor(max_workers=self.n_jobs) as e: - futures = [ - e.submit(find_duplicates, pi_set, other) - for pi_set, other in duplicate_inputs - ] - duplicate = [] + futures = [] + with concurrent.futures.ProcessPoolExecutor( + max_workers=self.n_jobs + ) as executor: + for num_set, pi_set in enumerate(self.__allpiterms): + dummy_other_terms.remove(pi_set) + for num_other, other in enumerate(dummy_other_terms): + futures.append( + executor.submit(find_duplicates_worker, pi_set, other) + ) + for future in tqdm.tqdm( concurrent.futures.as_completed(futures), total=len(futures) ): - duplicate.append(future.result()) + duplicate.extend(future.result()) # remove duplicates from the main dict of all pi terms for dup in duplicate: