From 6b40d90e8e221dcc06b312e5c9fe548281703fa4 Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 15:53:57 +0200 Subject: [PATCH 01/68] Name input layer xgrids --- n3fit/src/n3fit/model_gen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 2814c2f656..901ad73c41 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -534,7 +534,7 @@ def pdfNN_layer_generator( # Generate the generic layers that will not depend on extra considerations # First prepare the input for the PDF model and any scaling if needed - placeholder_input = Input(shape=(None, 1), batch_size=1) + placeholder_input = Input(shape=(None, 1), batch_size=1, name='xgrids') subtract_one = False process_input = Lambda(lambda x: x) @@ -546,7 +546,7 @@ def pdfNN_layer_generator( process_input = Lambda(lambda x: 2 * x - 1) subtract_one = True input_x_eq_1 = scaler([1.0])[0] - placeholder_input = Input(shape=(None, 2), batch_size=1) + placeholder_input = Input(shape=(None, 2), batch_size=1, name='xgrids') elif inp == 2: # If the input is of type (x, logx) # create a x --> (x, logx) layer to preppend to everything From 57f6552aa8d85a2244d5c406e34fc0acebae1606 Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 17:48:03 +0200 Subject: [PATCH 02/68] Rename and simplify dense_me -> neural_network --- n3fit/src/n3fit/model_gen.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 901ad73c41..47acb60074 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -597,15 +597,12 @@ def pdfNN_layer_generator( basis_size=last_layer_nodes, ) - def dense_me(x): + def neural_network(x): """Takes an input tensor `x` and applies all layers from the `list_of_pdf_layers` in order""" - processed_x = process_input(x) - curr_fun = list_of_pdf_layers[0](processed_x) - - for dense_layer in list_of_pdf_layers[1:]: - curr_fun = dense_layer(curr_fun) - return curr_fun + for layer in list_of_pdf_layers: + x = layer(x) + return x preproseed = layer_seed + number_of_layers layer_preproc = Preprocessing( @@ -624,9 +621,9 @@ def layer_fitbasis(x): x_scaled = op.op_gather_keep_dims(x, 0, axis=-1) x_original = op.op_gather_keep_dims(x, -1, axis=-1) - nn_output = dense_me(x_scaled) + nn_output = neural_network(process_input(x_scaled)) if subtract_one: - nn_at_one = dense_me(layer_x_eq_1) + nn_at_one = neural_network(process_input(layer_x_eq_1)) nn_output = op.op_subtract([nn_output, nn_at_one]) ret = op.op_multiply([nn_output, layer_preproc(x_original)]) From d81814d44fe9ee343ca4c721f729f28e2e5e2821 Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 17:58:23 +0200 Subject: [PATCH 03/68] Rename i->i_replica and layer_seed->replica_seed --- n3fit/src/n3fit/model_gen.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 47acb60074..1627206b46 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -570,9 +570,9 @@ def pdfNN_layer_generator( else: sumrule_layer = lambda x: x - # Now we need a trainable network per model to be trained in parallel + # Now we need a trainable network per replica to be trained in parallel pdf_models = [] - for i, layer_seed in enumerate(seed): + for i_replica, replica_seed in enumerate(seed): if layer_type == "dense": reg = regularizer_selector(regularizer, **regularizer_args) list_of_pdf_layers = generate_dense_network( @@ -580,7 +580,7 @@ def pdfNN_layer_generator( nodes, activations, initializer_name, - seed=layer_seed, + seed=replica_seed, dropout_rate=dropout, regularizer=reg, ) @@ -593,7 +593,7 @@ def pdfNN_layer_generator( nodes, activations, initializer_name, - seed=layer_seed, + seed=replica_seed, basis_size=last_layer_nodes, ) @@ -604,11 +604,11 @@ def neural_network(x): x = layer(x) return x - preproseed = layer_seed + number_of_layers + preproseed = replica_seed + number_of_layers layer_preproc = Preprocessing( flav_info=flav_info, input_shape=(1,), - name=f"pdf_prepro_{i}", + name=f"pdf_prepro_{i_replica}", seed=preproseed, large_x=not subtract_one, ) @@ -641,7 +641,7 @@ def layer_pdf(x): # Create the model pdf_model = MetaModel( - model_input, final_pdf(placeholder_input), name=f"PDF_{i}", scaler=scaler + model_input, final_pdf(placeholder_input), name=f"PDF_{i_replica}", scaler=scaler ) pdf_models.append(pdf_model) return pdf_models From 557b14c96d459c2cbe4ba9c9382c5c4ccd5503e3 Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 18:13:53 +0200 Subject: [PATCH 04/68] Rename preprocessing -> prefactor --- n3fit/src/n3fit/layers/__init__.py | 2 +- .../layers/{preprocessing.py => prefactor.py} | 16 ++++++++-------- n3fit/src/n3fit/model_gen.py | 16 ++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) rename n3fit/src/n3fit/layers/{preprocessing.py => prefactor.py} (91%) diff --git a/n3fit/src/n3fit/layers/__init__.py b/n3fit/src/n3fit/layers/__init__.py index e5839046cc..ccc78fbb15 100644 --- a/n3fit/src/n3fit/layers/__init__.py +++ b/n3fit/src/n3fit/layers/__init__.py @@ -1,4 +1,4 @@ -from .preprocessing import Preprocessing +from .prefactor import Prefactor from .rotations import FkRotation, FlavourToEvolution, ObsRotation from .x_operations import xIntegrator, xDivide from .msr_normalization import MSR_Normalization diff --git a/n3fit/src/n3fit/layers/preprocessing.py b/n3fit/src/n3fit/layers/prefactor.py similarity index 91% rename from n3fit/src/n3fit/layers/preprocessing.py rename to n3fit/src/n3fit/layers/prefactor.py index 56278224bf..9651c059a1 100644 --- a/n3fit/src/n3fit/layers/preprocessing.py +++ b/n3fit/src/n3fit/layers/prefactor.py @@ -3,13 +3,13 @@ from n3fit.backends import operations as op -class Preprocessing(MetaLayer): +class Prefactor(MetaLayer): """ - Applies preprocessing to the PDF. + Computes prefactor for the PDF. This layer generates a factor (1-x)^beta*x^(1-alpha) where both beta and alpha - are model paramters that can be trained. If feature scaling is used, the preprocessing - factor is x^(1-alpha). + are model paramters that can be trained. If feature scaling is used, the + prefactor is x^(1-alpha). Alpha is initialized uniformly within the ranges allowed in the runcard and then it is only allowed to move between those two values (with a hard wall in each side) @@ -21,7 +21,7 @@ class Preprocessing(MetaLayer): Parameters ---------- flav_info: list - list of dicts containing the information about the fitting of the preprocessing + list of dicts containing the information about the fitting of the prefactor This corresponds to the `fitting::basis` parameter in the nnpdf runcard. The dicts can contain the following fields: `smallx`: range of alpha @@ -29,7 +29,7 @@ class Preprocessing(MetaLayer): `trainable`: whether these alpha-beta should be trained during the fit (defaults to true) large_x: bool - Whether large x preprocessing should be active + Whether large x prefactor should be active seed: int seed for the initializer of the random alpha and beta values """ @@ -43,7 +43,7 @@ def __init__( **kwargs, ): if flav_info is None: - raise ValueError("Trying to instantiate a preprocessing with no basis information") + raise ValueError("Trying to instantiate a prefactor with no basis information") self.flav_info = flav_info self.seed = seed self.output_dim = len(flav_info) @@ -105,7 +105,7 @@ def build(self, input_shape): beta_name = f"beta_{flav_name}" self.generate_weight(beta_name, "largex", flav_dict, set_to_zero=not self.large_x) - super(Preprocessing, self).build(input_shape) + super(Prefactor, self).build(input_shape) def call(self, inputs, **kwargs): x = inputs diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 1627206b46..2fd1befbbf 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -13,7 +13,7 @@ import numpy as np from n3fit.msr import msr_impose from n3fit.layers import DIS, DY, ObsRotation, losses -from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution +from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution, Mask from n3fit.layers.observable import is_unique from n3fit.backends import MetaModel, Input @@ -409,7 +409,7 @@ def pdfNN_layer_generator( and outputs a basis of 14 PDFs. It generates the preprocessing of the x into a set (x, log(x)), the arbitrary NN to fit the form of the PDF - and the preprocessing factors. + and the prefactor. The funtional form of the output of this function is of: @@ -444,7 +444,7 @@ def pdfNN_layer_generator( A function is constructed that joins all those layers. The function takes a tensor as the input and applies all layers for NN in order. - 4. Create a preprocessing layer (that takes as input the same tensor x as the NN) + 4. Create a prefactor layer (that takes as input the same tensor x as the NN) and multiply it to the NN. We have now: N(x)_{j} * x^{1-alpha_{j}} * (1-x)^{beta_{j}} @@ -481,7 +481,7 @@ def pdfNN_layer_generator( selects the type of architecture of the NN. Default: dense flav_info: dict dictionary containing the information about each PDF (basis dictionary in the runcard) - to be used by Preprocessing + to be used by Prefactor out: int number of output flavours of the model (default 14) seed: list(int) @@ -604,12 +604,12 @@ def neural_network(x): x = layer(x) return x - preproseed = replica_seed + number_of_layers - layer_preproc = Preprocessing( + prefacseed = replica_seed + number_of_layers + layer_preproc = Prefactor( flav_info=flav_info, input_shape=(1,), - name=f"pdf_prepro_{i_replica}", - seed=preproseed, + name=f"pdf_prefactor_{i_replica}", + seed=prefacseed, large_x=not subtract_one, ) From e087a386729141c60012c0f3e20184c747e1e6dd Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 19:04:24 +0200 Subject: [PATCH 05/68] Create apply_prefactor layer (can't name yet as it is created twice rather than reused, in the sumrule) --- n3fit/src/n3fit/model_gen.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 2fd1befbbf..970825754a 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -573,6 +573,15 @@ def pdfNN_layer_generator( # Now we need a trainable network per replica to be trained in parallel pdf_models = [] for i_replica, replica_seed in enumerate(seed): + prefacseed = replica_seed + number_of_layers + compute_prefactor = Prefactor( + flav_info=flav_info, + input_shape=(1,), + name=f"pdf_prefactor_{i_replica}", + seed=prefacseed, + large_x=not subtract_one, + ) + if layer_type == "dense": reg = regularizer_selector(regularizer, **regularizer_args) list_of_pdf_layers = generate_dense_network( @@ -604,15 +613,6 @@ def neural_network(x): x = layer(x) return x - prefacseed = replica_seed + number_of_layers - layer_preproc = Prefactor( - flav_info=flav_info, - input_shape=(1,), - name=f"pdf_prefactor_{i_replica}", - seed=prefacseed, - large_x=not subtract_one, - ) - # Apply preprocessing and basis def layer_fitbasis(x): """The tensor x has a expected shape of (1, None, {1,2}) @@ -626,7 +626,9 @@ def layer_fitbasis(x): nn_at_one = neural_network(process_input(layer_x_eq_1)) nn_output = op.op_subtract([nn_output, nn_at_one]) - ret = op.op_multiply([nn_output, layer_preproc(x_original)]) + apply_prefactor = Lambda(op.op_multiply) + prefactor = compute_prefactor(x_original) + ret = apply_prefactor([nn_output, prefactor]) if basis_rotation.is_identity(): # if we don't need to rotate basis we don't want spurious layers return ret From ca187e4b608b51394c596423a6cdb52f26aaa1af Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 20:16:27 +0200 Subject: [PATCH 06/68] Merge msr.py into layers/msr_normalization.py --- n3fit/src/n3fit/layers/msr_normalization.py | 83 ++++++++++++++++++ n3fit/src/n3fit/model_gen.py | 2 +- n3fit/src/n3fit/msr.py | 93 --------------------- 3 files changed, 84 insertions(+), 94 deletions(-) delete mode 100644 n3fit/src/n3fit/msr.py diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index c31bf8fb41..5228b89360 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -1,3 +1,5 @@ +import numpy as np +from n3fit.layers import xDivide, xIntegrator from n3fit.backends import MetaLayer from n3fit.backends import operations as op @@ -64,3 +66,84 @@ def call(self, pdf_integrated): norm_constants += n_av + n_av3 + n_av8 + n_av15 return self._out_scatter(norm_constants) + +def gen_integration_input(nx): + """ + Generates a np.array (shaped (nx,1)) of nx elements where the + nx/2 first elements are a logspace between 0 and 0.1 + and the rest a linspace from 0.1 to 0 + """ + lognx = int(nx / 2) + linnx = int(nx - lognx) + xgrid_log = np.logspace(-9, -1, lognx + 1) + xgrid_lin = np.linspace(0.1, 1, linnx) + xgrid = np.concatenate([xgrid_log[:-1], xgrid_lin]).reshape(nx, 1) + + spacing = [0.0] + for i in range(1, nx): + spacing.append(np.abs(xgrid[i - 1] - xgrid[i])) + spacing.append(0.0) + + weights = [] + for i in range(nx): + weights.append((spacing[i] + spacing[i + 1]) / 2.0) + weights_array = np.array(weights).reshape(nx, 1) + + return xgrid, weights_array + + +def msr_impose(nx=int(2e3), mode='All', scaler=None): + """ + This function receives: + Generates a function that applies a normalization layer to the fit. + - fit_layer: the 8-basis layer of PDF which we fit + The normalization is computed from the direct output of the NN (so the 7,8-flavours basis) + - final_layer: the 14-basis which is fed to the fktable + and it is applied to the input of the fktable (i.e., to the 14-flavours fk-basis). + It uses pdf_fit to compute the sum rule and returns a modified version of + the final_pdf layer with a normalisation by which the sum rule is imposed + + Parameters + ---------- + nx: int + number of points for the integration grid, default: 2000 + mode: str + what sum rules to compute (MSR, VSR or All), default: All + scaler: scaler + Function to apply to the input. If given the input to the model + will be a (1, None, 2) tensor where dim [:,:,0] is scaled + """ + + # 1. Generate the fake input which will be used to integrate + xgrid, weights_array = gen_integration_input(nx) + # 1b If a scaler is provided, scale the input xgrid + if scaler: + xgrid = scaler(xgrid) + + # 2. Prepare the pdf for integration + # for that we need to multiply several flavours with 1/x + division_by_x = xDivide() + # 3. Now create the integration layer (the layer that will simply integrate, given some weight + integrator = xIntegrator(weights_array, input_shape=(nx,)) + + # 4. Now create the normalization by selecting the right integrations + normalizer = MSR_Normalization(mode=mode) + + # 5. Make the xgrid array into a backend input layer so it can be given to the normalization + xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") + # Finally prepare a function which will take as input the output of the PDF model + # and will return it appropiately normalized. + def apply_normalization(layer_pdf): + """ + layer_pdf: output of the PDF, unnormalized, ready for the fktable + """ + x_original = op.op_gather_keep_dims(xgrid_input, -1, axis=-1) + pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(xgrid_input)]) + normalization = normalizer(integrator(pdf_integrand)) + + def ultimate_pdf(x): + return layer_pdf(x)*normalization + + return ultimate_pdf + + return apply_normalization, xgrid_input diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 970825754a..3ac2d5db45 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -11,7 +11,7 @@ """ from dataclasses import dataclass import numpy as np -from n3fit.msr import msr_impose +from n3fit.layers.msr_normalization import msr_impose from n3fit.layers import DIS, DY, ObsRotation, losses from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution, Mask from n3fit.layers.observable import is_unique diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py deleted file mode 100644 index e99006de47..0000000000 --- a/n3fit/src/n3fit/msr.py +++ /dev/null @@ -1,93 +0,0 @@ -""" - The constraint module include functions to impose the momentum sum rules on the PDFs -""" -import logging -import numpy as np - -from n3fit.layers import xDivide, MSR_Normalization, xIntegrator -from n3fit.backends import operations as op - - -log = logging.getLogger(__name__) - - -def gen_integration_input(nx): - """ - Generates a np.array (shaped (nx,1)) of nx elements where the - nx/2 first elements are a logspace between 0 and 0.1 - and the rest a linspace from 0.1 to 0 - """ - lognx = int(nx / 2) - linnx = int(nx - lognx) - xgrid_log = np.logspace(-9, -1, lognx + 1) - xgrid_lin = np.linspace(0.1, 1, linnx) - xgrid = np.concatenate([xgrid_log[:-1], xgrid_lin]).reshape(nx, 1) - - spacing = [0.0] - for i in range(1, nx): - spacing.append(np.abs(xgrid[i - 1] - xgrid[i])) - spacing.append(0.0) - - weights = [] - for i in range(nx): - weights.append((spacing[i] + spacing[i + 1]) / 2.0) - weights_array = np.array(weights).reshape(nx, 1) - - return xgrid, weights_array - - -def msr_impose(nx=int(2e3), mode='All', scaler=None): - """ - This function receives: - Generates a function that applies a normalization layer to the fit. - - fit_layer: the 8-basis layer of PDF which we fit - The normalization is computed from the direct output of the NN (so the 7,8-flavours basis) - - final_layer: the 14-basis which is fed to the fktable - and it is applied to the input of the fktable (i.e., to the 14-flavours fk-basis). - It uses pdf_fit to compute the sum rule and returns a modified version of - the final_pdf layer with a normalisation by which the sum rule is imposed - - Parameters - ---------- - nx: int - number of points for the integration grid, default: 2000 - mode: str - what sum rules to compute (MSR, VSR or All), default: All - scaler: scaler - Function to apply to the input. If given the input to the model - will be a (1, None, 2) tensor where dim [:,:,0] is scaled - """ - - # 1. Generate the fake input which will be used to integrate - xgrid, weights_array = gen_integration_input(nx) - # 1b If a scaler is provided, scale the input xgrid - if scaler: - xgrid = scaler(xgrid) - - # 2. Prepare the pdf for integration - # for that we need to multiply several flavours with 1/x - division_by_x = xDivide() - # 3. Now create the integration layer (the layer that will simply integrate, given some weight - integrator = xIntegrator(weights_array, input_shape=(nx,)) - - # 4. Now create the normalization by selecting the right integrations - normalizer = MSR_Normalization(mode=mode) - - # 5. Make the xgrid array into a backend input layer so it can be given to the normalization - xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") - # Finally prepare a function which will take as input the output of the PDF model - # and will return it appropiately normalized. - def apply_normalization(layer_pdf): - """ - layer_pdf: output of the PDF, unnormalized, ready for the fktable - """ - x_original = op.op_gather_keep_dims(xgrid_input, -1, axis=-1) - pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(xgrid_input)]) - normalization = normalizer(integrator(pdf_integrand)) - - def ultimate_pdf(x): - return layer_pdf(x)*normalization - - return ultimate_pdf - - return apply_normalization, xgrid_input From 97d7409b772a4915422c2ea9c783d79d2e95d5d2 Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 20:43:19 +0200 Subject: [PATCH 07/68] Turn msr_impose into class method --- n3fit/src/n3fit/layers/msr_normalization.py | 110 ++++++++++---------- n3fit/src/n3fit/model_gen.py | 6 +- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 5228b89360..2c9e4723f2 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -67,6 +67,61 @@ def call(self, pdf_integrated): return self._out_scatter(norm_constants) + def msr_impose(self, nx=int(2e3), mode='All', scaler=None): + """ + This function receives: + Generates a function that applies a normalization layer to the fit. + - fit_layer: the 8-basis layer of PDF which we fit + The normalization is computed from the direct output of the NN (so the 7,8-flavours basis) + - final_layer: the 14-basis which is fed to the fktable + and it is applied to the input of the fktable (i.e., to the 14-flavours fk-basis). + It uses pdf_fit to compute the sum rule and returns a modified version of + the final_pdf layer with a normalisation by which the sum rule is imposed + + Parameters + ---------- + nx: int + number of points for the integration grid, default: 2000 + mode: str + what sum rules to compute (MSR, VSR or All), default: All + scaler: scaler + Function to apply to the input. If given the input to the model + will be a (1, None, 2) tensor where dim [:,:,0] is scaled + """ + + # 1. Generate the fake input which will be used to integrate + xgrid, weights_array = gen_integration_input(nx) + # 1b If a scaler is provided, scale the input xgrid + if scaler: + xgrid = scaler(xgrid) + + # 2. Prepare the pdf for integration + # for that we need to multiply several flavours with 1/x + division_by_x = xDivide() + # 3. Now create the integration layer (the layer that will simply integrate, given some weight + integrator = xIntegrator(weights_array, input_shape=(nx,)) + + # 4. Now create the normalization by selecting the right integrations + + # 5. Make the xgrid array into a backend input layer so it can be given to the normalization + xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") + # Finally prepare a function which will take as input the output of the PDF model + # and will return it appropiately normalized. + def apply_normalization(layer_pdf): + """ + layer_pdf: output of the PDF, unnormalized, ready for the fktable + """ + x_original = op.op_gather_keep_dims(xgrid_input, -1, axis=-1) + pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(xgrid_input)]) + normalization = self(integrator(pdf_integrand)) + + def ultimate_pdf(x): + return layer_pdf(x)*normalization + + return ultimate_pdf + + return apply_normalization, xgrid_input + def gen_integration_input(nx): """ Generates a np.array (shaped (nx,1)) of nx elements where the @@ -92,58 +147,3 @@ def gen_integration_input(nx): return xgrid, weights_array -def msr_impose(nx=int(2e3), mode='All', scaler=None): - """ - This function receives: - Generates a function that applies a normalization layer to the fit. - - fit_layer: the 8-basis layer of PDF which we fit - The normalization is computed from the direct output of the NN (so the 7,8-flavours basis) - - final_layer: the 14-basis which is fed to the fktable - and it is applied to the input of the fktable (i.e., to the 14-flavours fk-basis). - It uses pdf_fit to compute the sum rule and returns a modified version of - the final_pdf layer with a normalisation by which the sum rule is imposed - - Parameters - ---------- - nx: int - number of points for the integration grid, default: 2000 - mode: str - what sum rules to compute (MSR, VSR or All), default: All - scaler: scaler - Function to apply to the input. If given the input to the model - will be a (1, None, 2) tensor where dim [:,:,0] is scaled - """ - - # 1. Generate the fake input which will be used to integrate - xgrid, weights_array = gen_integration_input(nx) - # 1b If a scaler is provided, scale the input xgrid - if scaler: - xgrid = scaler(xgrid) - - # 2. Prepare the pdf for integration - # for that we need to multiply several flavours with 1/x - division_by_x = xDivide() - # 3. Now create the integration layer (the layer that will simply integrate, given some weight - integrator = xIntegrator(weights_array, input_shape=(nx,)) - - # 4. Now create the normalization by selecting the right integrations - normalizer = MSR_Normalization(mode=mode) - - # 5. Make the xgrid array into a backend input layer so it can be given to the normalization - xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") - # Finally prepare a function which will take as input the output of the PDF model - # and will return it appropiately normalized. - def apply_normalization(layer_pdf): - """ - layer_pdf: output of the PDF, unnormalized, ready for the fktable - """ - x_original = op.op_gather_keep_dims(xgrid_input, -1, axis=-1) - pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(xgrid_input)]) - normalization = normalizer(integrator(pdf_integrand)) - - def ultimate_pdf(x): - return layer_pdf(x)*normalization - - return ultimate_pdf - - return apply_normalization, xgrid_input diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 3ac2d5db45..59775cf56e 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -11,9 +11,8 @@ """ from dataclasses import dataclass import numpy as np -from n3fit.layers.msr_normalization import msr_impose from n3fit.layers import DIS, DY, ObsRotation, losses -from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution, Mask +from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution, Mask, MSR_Normalization from n3fit.layers.observable import is_unique from n3fit.backends import MetaModel, Input @@ -565,7 +564,8 @@ def pdfNN_layer_generator( # Normalization and sum rules if impose_sumrule: - sumrule_layer, integrator_input = msr_impose(mode=impose_sumrule, scaler=scaler) + msr_normalization = MSR_Normalization(mode=impose_sumrule) + sumrule_layer, integrator_input = msr_normalization.msr_impose(mode=impose_sumrule, scaler=scaler) model_input["integrator_input"] = integrator_input else: sumrule_layer = lambda x: x From e0fb2f5146371660acfd395314f0a980bf9377ad Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 20:53:18 +0200 Subject: [PATCH 08/68] Use MSR_Normalization class attributes --- n3fit/src/n3fit/layers/msr_normalization.py | 65 ++++++++++----------- n3fit/src/n3fit/model_gen.py | 2 +- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 2c9e4723f2..2fd9d54672 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -18,7 +18,9 @@ class MSR_Normalization(MetaLayer): _msr_enabled = False _vsr_enabled = False - def __init__(self, output_dim=14, mode="ALL", **kwargs): + def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs): + self.nx = nx + self.scaler = scaler if mode == True or mode.upper() == "ALL": self._msr_enabled = True self._vsr_enabled = True @@ -67,7 +69,7 @@ def call(self, pdf_integrated): return self._out_scatter(norm_constants) - def msr_impose(self, nx=int(2e3), mode='All', scaler=None): + def msr_impose(self): """ This function receives: Generates a function that applies a normalization layer to the fit. @@ -80,26 +82,22 @@ def msr_impose(self, nx=int(2e3), mode='All', scaler=None): Parameters ---------- - nx: int - number of points for the integration grid, default: 2000 - mode: str - what sum rules to compute (MSR, VSR or All), default: All scaler: scaler Function to apply to the input. If given the input to the model will be a (1, None, 2) tensor where dim [:,:,0] is scaled """ # 1. Generate the fake input which will be used to integrate - xgrid, weights_array = gen_integration_input(nx) + xgrid, weights_array = self._gen_integration_input() # 1b If a scaler is provided, scale the input xgrid - if scaler: - xgrid = scaler(xgrid) + if self.scaler: + xgrid = self.scaler(xgrid) # 2. Prepare the pdf for integration # for that we need to multiply several flavours with 1/x division_by_x = xDivide() # 3. Now create the integration layer (the layer that will simply integrate, given some weight - integrator = xIntegrator(weights_array, input_shape=(nx,)) + integrator = xIntegrator(weights_array, input_shape=(self.nx,)) # 4. Now create the normalization by selecting the right integrations @@ -122,28 +120,27 @@ def ultimate_pdf(x): return apply_normalization, xgrid_input -def gen_integration_input(nx): - """ - Generates a np.array (shaped (nx,1)) of nx elements where the - nx/2 first elements are a logspace between 0 and 0.1 - and the rest a linspace from 0.1 to 0 - """ - lognx = int(nx / 2) - linnx = int(nx - lognx) - xgrid_log = np.logspace(-9, -1, lognx + 1) - xgrid_lin = np.linspace(0.1, 1, linnx) - xgrid = np.concatenate([xgrid_log[:-1], xgrid_lin]).reshape(nx, 1) - - spacing = [0.0] - for i in range(1, nx): - spacing.append(np.abs(xgrid[i - 1] - xgrid[i])) - spacing.append(0.0) - - weights = [] - for i in range(nx): - weights.append((spacing[i] + spacing[i + 1]) / 2.0) - weights_array = np.array(weights).reshape(nx, 1) - - return xgrid, weights_array - + def _gen_integration_input(self): + """ + Generates a np.array (shaped (nx,1)) of nx elements where the + nx/2 first elements are a logspace between 0 and 0.1 + and the rest a linspace from 0.1 to 0 + """ + lognx = int(self.nx / 2) + linnx = int(self.nx - lognx) + xgrid_log = np.logspace(-9, -1, lognx + 1) + xgrid_lin = np.linspace(0.1, 1, linnx) + xgrid = np.concatenate([xgrid_log[:-1], xgrid_lin]).reshape(self.nx, 1) + + spacing = [0.0] + for i in range(1, self.nx): + spacing.append(np.abs(xgrid[i - 1] - xgrid[i])) + spacing.append(0.0) + + weights = [] + for i in range(self.nx): + weights.append((spacing[i] + spacing[i + 1]) / 2.0) + weights_array = np.array(weights).reshape(self.nx, 1) + + return xgrid, weights_array diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 59775cf56e..3ed1d53dce 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -565,7 +565,7 @@ def pdfNN_layer_generator( # Normalization and sum rules if impose_sumrule: msr_normalization = MSR_Normalization(mode=impose_sumrule) - sumrule_layer, integrator_input = msr_normalization.msr_impose(mode=impose_sumrule, scaler=scaler) + sumrule_layer, integrator_input = msr_normalization.msr_impose() model_input["integrator_input"] = integrator_input else: sumrule_layer = lambda x: x From c8e7e63383a2254c4a73363746ac66b4c5f0e234 Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 20:59:50 +0200 Subject: [PATCH 09/68] Create integration grid in MSR_Normalizations init --- n3fit/src/n3fit/layers/msr_normalization.py | 28 ++++++++++----------- n3fit/src/n3fit/model_gen.py | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 2fd9d54672..ee4993093f 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -41,6 +41,8 @@ def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs op.scatter_to_one, op_kwargs={"indices": idx, "output_dim": output_dim} ) + self._gen_integration_input() + super().__init__(**kwargs, name="normalizer") def call(self, pdf_integrated): @@ -87,30 +89,22 @@ def msr_impose(self): will be a (1, None, 2) tensor where dim [:,:,0] is scaled """ - # 1. Generate the fake input which will be used to integrate - xgrid, weights_array = self._gen_integration_input() - # 1b If a scaler is provided, scale the input xgrid - if self.scaler: - xgrid = self.scaler(xgrid) - # 2. Prepare the pdf for integration # for that we need to multiply several flavours with 1/x division_by_x = xDivide() # 3. Now create the integration layer (the layer that will simply integrate, given some weight - integrator = xIntegrator(weights_array, input_shape=(self.nx,)) + integrator = xIntegrator(self.weights_array, input_shape=(self.nx,)) # 4. Now create the normalization by selecting the right integrations - # 5. Make the xgrid array into a backend input layer so it can be given to the normalization - xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") # Finally prepare a function which will take as input the output of the PDF model # and will return it appropiately normalized. def apply_normalization(layer_pdf): """ layer_pdf: output of the PDF, unnormalized, ready for the fktable """ - x_original = op.op_gather_keep_dims(xgrid_input, -1, axis=-1) - pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(xgrid_input)]) + x_original = op.op_gather_keep_dims(self.xgrid_input, -1, axis=-1) + pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(self.xgrid_input)]) normalization = self(integrator(pdf_integrand)) def ultimate_pdf(x): @@ -118,7 +112,7 @@ def ultimate_pdf(x): return ultimate_pdf - return apply_normalization, xgrid_input + return apply_normalization def _gen_integration_input(self): """ @@ -140,7 +134,13 @@ def _gen_integration_input(self): weights = [] for i in range(self.nx): weights.append((spacing[i] + spacing[i + 1]) / 2.0) - weights_array = np.array(weights).reshape(self.nx, 1) + self.weights_array = np.array(weights).reshape(self.nx, 1) - return xgrid, weights_array + # 1. Generate the fake input which will be used to integrate + # 1b If a scaler is provided, scale the input xgrid + if self.scaler: + xgrid = self.scaler(xgrid) + + # 5. Make the xgrid array into a backend input layer so it can be given to the normalization + self.xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 3ed1d53dce..35bc049f47 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -565,8 +565,8 @@ def pdfNN_layer_generator( # Normalization and sum rules if impose_sumrule: msr_normalization = MSR_Normalization(mode=impose_sumrule) - sumrule_layer, integrator_input = msr_normalization.msr_impose() - model_input["integrator_input"] = integrator_input + sumrule_layer = msr_normalization.msr_impose() + model_input["integrator_input"] = msr_normalization.xgrid_input else: sumrule_layer = lambda x: x From c5551f11d989159d9f7e2d598b103e3c108f5b66 Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 21:23:53 +0200 Subject: [PATCH 10/68] Renamings --- n3fit/src/n3fit/layers/msr_normalization.py | 25 ++++++++++++--------- n3fit/src/n3fit/model_gen.py | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index ee4993093f..711b066716 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -42,6 +42,12 @@ def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs ) self._gen_integration_input() + # 2. Prepare the pdf for integration + # for that we need to multiply several flavours with 1/x + self.divide_by_x = xDivide() + # 3. Now create the integration layer (the layer that will simply integrate, given some weight + self.integrator = xIntegrator(self.weights_array, input_shape=(self.nx,)) + super().__init__(**kwargs, name="normalizer") @@ -89,12 +95,6 @@ def msr_impose(self): will be a (1, None, 2) tensor where dim [:,:,0] is scaled """ - # 2. Prepare the pdf for integration - # for that we need to multiply several flavours with 1/x - division_by_x = xDivide() - # 3. Now create the integration layer (the layer that will simply integrate, given some weight - integrator = xIntegrator(self.weights_array, input_shape=(self.nx,)) - # 4. Now create the normalization by selecting the right integrations # Finally prepare a function which will take as input the output of the PDF model @@ -103,12 +103,15 @@ def apply_normalization(layer_pdf): """ layer_pdf: output of the PDF, unnormalized, ready for the fktable """ - x_original = op.op_gather_keep_dims(self.xgrid_input, -1, axis=-1) - pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(self.xgrid_input)]) - normalization = self(integrator(pdf_integrand)) + x_original = op.op_gather_keep_dims(self.xgrid_integration, -1, axis=-1) + x_divided = self.divide_by_x(x_original) + pdf_xgrid_integration = layer_pdf(self.xgrid_integration) + pdf_integrand = op.op_multiply([x_divided, pdf_xgrid_integration]) + normalization_factor = self(self.integrator(pdf_integrand)) def ultimate_pdf(x): - return layer_pdf(x)*normalization + pdf_xgrid = layer_pdf(x) + return normalization_factor * pdf_xgrid return ultimate_pdf @@ -142,5 +145,5 @@ def _gen_integration_input(self): xgrid = self.scaler(xgrid) # 5. Make the xgrid array into a backend input layer so it can be given to the normalization - self.xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") + self.xgrid_integration = op.numpy_to_input(xgrid, name="integration_grid") diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 35bc049f47..11e18713c1 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -566,7 +566,7 @@ def pdfNN_layer_generator( if impose_sumrule: msr_normalization = MSR_Normalization(mode=impose_sumrule) sumrule_layer = msr_normalization.msr_impose() - model_input["integrator_input"] = msr_normalization.xgrid_input + model_input["integrator_input"] = msr_normalization.xgrid_integration else: sumrule_layer = lambda x: x From 62416143e6756f49d7253a9a78adb718ec65206e Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 12 May 2023 21:49:17 +0200 Subject: [PATCH 11/68] Prepare call method in MSR_Normalization --- n3fit/src/n3fit/layers/msr_normalization.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 711b066716..581b9abe3d 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -103,20 +103,26 @@ def apply_normalization(layer_pdf): """ layer_pdf: output of the PDF, unnormalized, ready for the fktable """ - x_original = op.op_gather_keep_dims(self.xgrid_integration, -1, axis=-1) - x_divided = self.divide_by_x(x_original) pdf_xgrid_integration = layer_pdf(self.xgrid_integration) - pdf_integrand = op.op_multiply([x_divided, pdf_xgrid_integration]) - normalization_factor = self(self.integrator(pdf_integrand)) def ultimate_pdf(x): pdf_xgrid = layer_pdf(x) - return normalization_factor * pdf_xgrid + return self.tempcall([pdf_xgrid, pdf_xgrid_integration]) return ultimate_pdf return apply_normalization + def tempcall(self, pdfx_pdfinteg): + pdf_xgrid, pdf_xgrid_integration = pdfx_pdfinteg + + x_original = op.op_gather_keep_dims(self.xgrid_integration, -1, axis=-1) + x_divided = self.divide_by_x(x_original) + pdf_integrand = op.op_multiply([x_divided, pdf_xgrid_integration]) + normalization_factor = self(self.integrator(pdf_integrand)) + + return normalization_factor * pdf_xgrid + def _gen_integration_input(self): """ Generates a np.array (shaped (nx,1)) of nx elements where the From 4ecb1baf8e50136d40bc098bab2102feeb2f9b70 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 15 May 2023 13:53:05 +0200 Subject: [PATCH 12/68] Share more layers, add names --- n3fit/src/n3fit/model_gen.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 11e18713c1..4692139298 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -536,20 +536,29 @@ def pdfNN_layer_generator( placeholder_input = Input(shape=(None, 1), batch_size=1, name='xgrids') subtract_one = False - process_input = Lambda(lambda x: x) + process_input = Lambda(lambda x: x, name='process_input') input_x_eq_1 = [1.0] # When scaler is active we also want to do the subtraction of large x # TODO: make it its own option (i.e., one could want to use this without using scaler) if scaler: # change the input domain [0,1] -> [-1,1] - process_input = Lambda(lambda x: 2 * x - 1) + process_input = Lambda(lambda x: 2 * x - 1, name='process_input') subtract_one = True input_x_eq_1 = scaler([1.0])[0] placeholder_input = Input(shape=(None, 2), batch_size=1, name='xgrids') elif inp == 2: # If the input is of type (x, logx) # create a x --> (x, logx) layer to preppend to everything - process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1)) + process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='process_input') + + extract_scaled = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') + extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') + + # the layer that multiplies the NN output by the prefactor + apply_prefactor = Lambda(op.op_multiply, name='apply_prefactor') + + # the layer that subtracts 1 from the NN output + subtract_one_layer = Lambda(op.op_subtract, name='subtract_one') model_input = {"pdf_input": placeholder_input} if subtract_one: @@ -614,25 +623,25 @@ def neural_network(x): return x # Apply preprocessing and basis + def layer_fitbasis(x): """The tensor x has a expected shape of (1, None, {1,2}) where x[...,0] corresponds to the feature_scaled input and x[...,-1] the original input """ - x_scaled = op.op_gather_keep_dims(x, 0, axis=-1) - x_original = op.op_gather_keep_dims(x, -1, axis=-1) + x_scaled = extract_scaled(x) + x_original = extract_original(x) nn_output = neural_network(process_input(x_scaled)) if subtract_one: nn_at_one = neural_network(process_input(layer_x_eq_1)) - nn_output = op.op_subtract([nn_output, nn_at_one]) + nn_output = subtract_one_layer([nn_output, nn_at_one]) - apply_prefactor = Lambda(op.op_multiply) prefactor = compute_prefactor(x_original) ret = apply_prefactor([nn_output, prefactor]) - if basis_rotation.is_identity(): + if not basis_rotation.is_identity(): # if we don't need to rotate basis we don't want spurious layers - return ret - return basis_rotation(ret) + ret = basis_rotation(ret) + return ret # Rotation layer, changes from the 8-basis to the 14-basis def layer_pdf(x): From 767977b8e8ccbfdba6a774d57e462500d0d511c9 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 15 May 2023 14:40:24 +0200 Subject: [PATCH 13/68] Create named layers in msr_normalization --- n3fit/src/n3fit/layers/msr_normalization.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 581b9abe3d..e20fae5d89 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -1,6 +1,6 @@ import numpy as np from n3fit.layers import xDivide, xIntegrator -from n3fit.backends import MetaLayer +from n3fit.backends import MetaLayer, Lambda from n3fit.backends import operations as op GLUON_IDX = [[2]] @@ -44,10 +44,13 @@ def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs self._gen_integration_input() # 2. Prepare the pdf for integration # for that we need to multiply several flavours with 1/x + self.get_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ") self.divide_by_x = xDivide() # 3. Now create the integration layer (the layer that will simply integrate, given some weight self.integrator = xIntegrator(self.weights_array, input_shape=(self.nx,)) + self.compute_integrand = Lambda(op.op_multiply, name="pdf_integrand") + self.compute_normalized_pdf = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized") super().__init__(**kwargs, name="normalizer") @@ -116,12 +119,13 @@ def ultimate_pdf(x): def tempcall(self, pdfx_pdfinteg): pdf_xgrid, pdf_xgrid_integration = pdfx_pdfinteg - x_original = op.op_gather_keep_dims(self.xgrid_integration, -1, axis=-1) + x_original = self.get_original(self.xgrid_integration) x_divided = self.divide_by_x(x_original) - pdf_integrand = op.op_multiply([x_divided, pdf_xgrid_integration]) + pdf_integrand = self.compute_integrand([x_divided, pdf_xgrid_integration]) normalization_factor = self(self.integrator(pdf_integrand)) - return normalization_factor * pdf_xgrid + pdf_normalized = self.compute_normalized_pdf([pdf_xgrid, normalization_factor]) + return pdf_normalized def _gen_integration_input(self): """ From d6ed37c23dc37cf39876d9fa5e22feb2faa32410 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 15 May 2023 15:41:20 +0200 Subject: [PATCH 14/68] Add pdf_integrated step --- n3fit/src/n3fit/layers/msr_normalization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index e20fae5d89..b068859045 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -122,7 +122,8 @@ def tempcall(self, pdfx_pdfinteg): x_original = self.get_original(self.xgrid_integration) x_divided = self.divide_by_x(x_original) pdf_integrand = self.compute_integrand([x_divided, pdf_xgrid_integration]) - normalization_factor = self(self.integrator(pdf_integrand)) + pdf_integrated = self.integrator(pdf_integrand) + normalization_factor = self(pdf_integrated) pdf_normalized = self.compute_normalized_pdf([pdf_xgrid, normalization_factor]) return pdf_normalized From 396fc6f40b64f662cb95de17f83fb621f5e805c0 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 15 May 2023 15:48:48 +0200 Subject: [PATCH 15/68] Move computation of xdivided to init, move integration into call --- n3fit/src/n3fit/layers/msr_normalization.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index b068859045..ce6341fc75 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -46,6 +46,8 @@ def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs # for that we need to multiply several flavours with 1/x self.get_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ") self.divide_by_x = xDivide() + x_original = self.get_original(self.xgrid_integration) + self.x_divided = self.divide_by_x(x_original) # 3. Now create the integration layer (the layer that will simply integrate, given some weight self.integrator = xIntegrator(self.weights_array, input_shape=(self.nx,)) @@ -54,7 +56,7 @@ def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs super().__init__(**kwargs, name="normalizer") - def call(self, pdf_integrated): + def call(self, pdf_integrand): """Imposes the valence and momentum sum rules: A_g = (1-sigma)/g A_v = A_v24 = A_v35 = 3/V @@ -64,6 +66,7 @@ def call(self, pdf_integrated): Note that both the input and the output are in the 14-flavours fk-basis """ + pdf_integrated = self.integrator(pdf_integrand) y = op.flatten(pdf_integrated) norm_constants = [] @@ -119,11 +122,8 @@ def ultimate_pdf(x): def tempcall(self, pdfx_pdfinteg): pdf_xgrid, pdf_xgrid_integration = pdfx_pdfinteg - x_original = self.get_original(self.xgrid_integration) - x_divided = self.divide_by_x(x_original) - pdf_integrand = self.compute_integrand([x_divided, pdf_xgrid_integration]) - pdf_integrated = self.integrator(pdf_integrand) - normalization_factor = self(pdf_integrated) + pdf_integrand = self.compute_integrand([self.x_divided, pdf_xgrid_integration]) + normalization_factor = self(pdf_integrand) pdf_normalized = self.compute_normalized_pdf([pdf_xgrid, normalization_factor]) return pdf_normalized From 472abed13add183c50259f630b64ab666edf2f90 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 15 May 2023 15:52:43 +0200 Subject: [PATCH 16/68] remove tempcall --- n3fit/src/n3fit/layers/msr_normalization.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index ce6341fc75..7d15e9073f 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -112,22 +112,17 @@ def apply_normalization(layer_pdf): pdf_xgrid_integration = layer_pdf(self.xgrid_integration) def ultimate_pdf(x): + pdf_integrand = self.compute_integrand([self.x_divided, pdf_xgrid_integration]) + normalization_factor = self(pdf_integrand) + pdf_xgrid = layer_pdf(x) - return self.tempcall([pdf_xgrid, pdf_xgrid_integration]) + pdf_normalized = self.compute_normalized_pdf([pdf_xgrid, normalization_factor]) + return pdf_normalized return ultimate_pdf return apply_normalization - def tempcall(self, pdfx_pdfinteg): - pdf_xgrid, pdf_xgrid_integration = pdfx_pdfinteg - - pdf_integrand = self.compute_integrand([self.x_divided, pdf_xgrid_integration]) - normalization_factor = self(pdf_integrand) - - pdf_normalized = self.compute_normalized_pdf([pdf_xgrid, normalization_factor]) - return pdf_normalized - def _gen_integration_input(self): """ Generates a np.array (shaped (nx,1)) of nx elements where the From 08c887655e644c8b8b7e3edf128ee662c9e440a7 Mon Sep 17 00:00:00 2001 From: Aron Date: Tue, 16 May 2023 13:43:57 +0200 Subject: [PATCH 17/68] Fix bug introduced after renaming preprocessing to prefactor --- n3fit/src/n3fit/io/writer.py | 2 +- n3fit/src/n3fit/vpinterface.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/n3fit/src/n3fit/io/writer.py b/n3fit/src/n3fit/io/writer.py index 6b4879f5a8..32418e9543 100644 --- a/n3fit/src/n3fit/io/writer.py +++ b/n3fit/src/n3fit/io/writer.py @@ -318,7 +318,7 @@ def jsonfit(replica_status, pdf_object, tr_chi2, vl_chi2, true_chi2, stop_epoch, """ all_info = {} # Generate preprocessing information - all_info["preprocessing"] = pdf_object.get_preprocessing_factors() + all_info["preprocessing"] = pdf_object.get_prefactor_factors() # .fitinfo-like info all_info["stop_epoch"] = stop_epoch all_info["best_epoch"] = replica_status.best_epoch diff --git a/n3fit/src/n3fit/vpinterface.py b/n3fit/src/n3fit/vpinterface.py index 777cac91ac..f851aafb3f 100644 --- a/n3fit/src/n3fit/vpinterface.py +++ b/n3fit/src/n3fit/vpinterface.py @@ -195,29 +195,31 @@ def get_nn_weights(self): """Outputs all weights of the NN as numpy.ndarrays""" return [model.get_weights() for model in self._models] - def get_preprocessing_factors(self, replica=None): - """Loads the preprocessing alpha and beta arrays from the PDF trained model. + def get_prefactor_factors(self, replica=None): + """Loads the prefactor alpha and beta arrays from the PDF trained model. If a ``fit_basis`` given in the format of ``n3fit`` runcards is given it will be used to generate a new dictionary with the names, the exponent and whether they are trainable otherwise outputs a Nx2 array where [:,0] are alphas and [:,1] betas """ - # If no replica is explicitly requested, get the preprocessing layer for the first model + # If no replica is explicitly requested, get the prefactor layer for the first model if replica is None: replica = 1 # Replicas start counting in 1 so: - preprocessing_layers = self._models[replica - 1].get_layer_re(r"pdf_prepro_\d") - if len(preprocessing_layers) != 1: + prefactor_layers = self._models[replica - 1].get_layer_re(r"pdf_prefactor_\d") + if len(prefactor_layers) > 1: # We really don't want to fail at this point, but print a warning at least... - log.warning("More than one preprocessing layer found within the model!") - preprocessing_layer = preprocessing_layers[0] + log.warning("More than one prefactor layer found within the model!") + elif len(prefactor_layers) < 1: + log.warning("No prefactor layer found within the model!") + prefactor_layer = prefactor_layers[0] alphas_and_betas = None if self.fit_basis is not None: output_dictionaries = [] for d in self.fit_basis: flavour = d["fl"] - alpha = preprocessing_layer.get_weight_by_name(f"alpha_{flavour}") - beta = preprocessing_layer.get_weight_by_name(f"beta_{flavour}") + alpha = prefactor_layer.get_weight_by_name(f"alpha_{flavour}") + beta = prefactor_layer.get_weight_by_name(f"beta_{flavour}") if alpha is not None: alpha = float(alpha.numpy()) if beta is not None: From 5337d184bfa26680d04fd251067903a4d318df96 Mon Sep 17 00:00:00 2001 From: Aron Date: Tue, 16 May 2023 15:38:39 +0200 Subject: [PATCH 18/68] Join neural network layers into their own NN_i model --- n3fit/src/n3fit/model_gen.py | 102 +++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 4692139298..9f63c26e29 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -9,6 +9,7 @@ """ +from typing import List from dataclasses import dataclass import numpy as np from n3fit.layers import DIS, DY, ObsRotation, losses @@ -387,21 +388,21 @@ def output_layer(ilayer): def pdfNN_layer_generator( - inp=2, - nodes=None, - activations=None, - initializer_name="glorot_normal", - layer_type="dense", - flav_info=None, + inp: int = 2, + nodes: List[int] = None, + activations: List[str] = None, + initializer_name: str = "glorot_normal", + layer_type: str = "dense", + flav_info: dict = None, fitbasis="NN31IC", - out=14, - seed=None, - dropout=0.0, + out: int = 14, + seed: int = None, + dropout: float = 0.0, regularizer=None, regularizer_args=None, - impose_sumrule=None, + impose_sumrule: str = None, scaler=None, - parallel_models=1, + parallel_models: int = 1, ): # pylint: disable=too-many-locals """ Generates the PDF model which takes as input a point in x (from 0 to 1) @@ -582,48 +583,21 @@ def pdfNN_layer_generator( # Now we need a trainable network per replica to be trained in parallel pdf_models = [] for i_replica, replica_seed in enumerate(seed): - prefacseed = replica_seed + number_of_layers + compute_prefactor = Prefactor( flav_info=flav_info, input_shape=(1,), name=f"pdf_prefactor_{i_replica}", - seed=prefacseed, + seed=replica_seed + number_of_layers, large_x=not subtract_one, ) - if layer_type == "dense": - reg = regularizer_selector(regularizer, **regularizer_args) - list_of_pdf_layers = generate_dense_network( - inp, - nodes, - activations, - initializer_name, - seed=replica_seed, - dropout_rate=dropout, - regularizer=reg, - ) - elif layer_type == "dense_per_flavour": - # Define the basis size attending to the last layer in the network - # TODO: this information should come from the basis information - # once the basis information is passed to this class - list_of_pdf_layers = generate_dense_per_flavour_network( - inp, - nodes, - activations, - initializer_name, - seed=replica_seed, - basis_size=last_layer_nodes, - ) - - def neural_network(x): - """Takes an input tensor `x` and applies all layers - from the `list_of_pdf_layers` in order""" - for layer in list_of_pdf_layers: - x = layer(x) - return x + neural_network = generate_nn( + layer_type, inp, nodes, activations, initializer_name, + replica_seed, dropout, regularizer, regularizer_args, + last_layer_nodes, name=f"NN_{i_replica}") # Apply preprocessing and basis - def layer_fitbasis(x): """The tensor x has a expected shape of (1, None, {1,2}) where x[...,0] corresponds to the feature_scaled input and x[...,-1] the original input @@ -650,9 +624,43 @@ def layer_pdf(x): # Final PDF (apply normalization) final_pdf = sumrule_layer(layer_pdf) + model_output = final_pdf(placeholder_input) + # Create the model - pdf_model = MetaModel( - model_input, final_pdf(placeholder_input), name=f"PDF_{i_replica}", scaler=scaler - ) + pdf_model = MetaModel(model_input, model_output, name=f"PDF_{i_replica}", scaler=scaler) pdf_models.append(pdf_model) return pdf_models + +def generate_nn( + layer_type: str, + inp: int, + nodes: List[int], + activations: List[str], + initializer_name: str, + replica_seed: int, + dropout: float, + regularizer: str, + regularizer_args: dict, + last_layer_nodes: int, + name: str) -> MetaModel: + """ + Create the part of the model that contains all of the actual neural network + layers. + """ + common_args = {'nodes_in': inp, 'nodes': nodes, 'activations': activations, 'initializer_name': initializer_name, 'seed': replica_seed} + if layer_type == "dense": + reg = regularizer_selector(regularizer, **regularizer_args) + list_of_pdf_layers = generate_dense_network(**common_args, dropout_rate=dropout, regularizer=reg) + elif layer_type == "dense_per_flavour": + list_of_pdf_layers = generate_dense_per_flavour_network(**common_args, basis_size=last_layer_nodes) + + # Note: using a Sequential model would be more appropriate, but it would require + # creating a MetaSequential model. + x = Input(shape=(None, inp), batch_size=1, name='xgrids_processed') + pdf = x + for layer in list_of_pdf_layers: + pdf = layer(pdf) + + model = MetaModel({'NN_input': x}, pdf, name=name) + return model + From ed8538932a4630f71b4212c3e7869572c5817f57 Mon Sep 17 00:00:00 2001 From: Aron Date: Tue, 16 May 2023 16:50:35 +0200 Subject: [PATCH 19/68] Clarify layer names --- n3fit/src/n3fit/layers/msr_normalization.py | 2 +- n3fit/src/n3fit/model_gen.py | 18 +++++++++--------- n3fit/src/n3fit/vpinterface.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 7d15e9073f..1c78f7877f 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -54,7 +54,7 @@ def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs self.compute_integrand = Lambda(op.op_multiply, name="pdf_integrand") self.compute_normalized_pdf = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized") - super().__init__(**kwargs, name="normalizer") + super().__init__(**kwargs, name="msr_weights") def call(self, pdf_integrand): """Imposes the valence and momentum sum rules: diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 9f63c26e29..f7b7236025 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -534,7 +534,7 @@ def pdfNN_layer_generator( # Generate the generic layers that will not depend on extra considerations # First prepare the input for the PDF model and any scaling if needed - placeholder_input = Input(shape=(None, 1), batch_size=1, name='xgrids') + placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') subtract_one = False process_input = Lambda(lambda x: x, name='process_input') @@ -546,17 +546,17 @@ def pdfNN_layer_generator( process_input = Lambda(lambda x: 2 * x - 1, name='process_input') subtract_one = True input_x_eq_1 = scaler([1.0])[0] - placeholder_input = Input(shape=(None, 2), batch_size=1, name='xgrids') + placeholder_input = Input(shape=(None, 2), batch_size=1, name='x') elif inp == 2: # If the input is of type (x, logx) # create a x --> (x, logx) layer to preppend to everything - process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='process_input') + process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='x_logx') extract_scaled = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') # the layer that multiplies the NN output by the prefactor - apply_prefactor = Lambda(op.op_multiply, name='apply_prefactor') + apply_prefactor = Lambda(op.op_multiply, name='prefactor_times_NN') # the layer that subtracts 1 from the NN output subtract_one_layer = Lambda(op.op_subtract, name='subtract_one') @@ -566,11 +566,11 @@ def pdfNN_layer_generator( layer_x_eq_1 = op.numpy_to_input(np.array(input_x_eq_1).reshape(1, 1)) model_input["layer_x_eq_1"] = layer_x_eq_1 - # Evolution layer - layer_evln = FkRotation(input_shape=(last_layer_nodes,), output_dim=out) - # Basis rotation - basis_rotation = FlavourToEvolution(flav_info=flav_info, fitbasis=fitbasis) + basis_rotation = FlavourToEvolution(flav_info=flav_info, fitbasis=fitbasis, name="pdf_evolution_basis") + + # Evolution layer + layer_evln = FkRotation(input_shape=(last_layer_nodes,), output_dim=out, name="pdf_FK_basis") # Normalization and sum rules if impose_sumrule: @@ -587,7 +587,7 @@ def pdfNN_layer_generator( compute_prefactor = Prefactor( flav_info=flav_info, input_shape=(1,), - name=f"pdf_prefactor_{i_replica}", + name=f"prefactor_{i_replica}", seed=replica_seed + number_of_layers, large_x=not subtract_one, ) diff --git a/n3fit/src/n3fit/vpinterface.py b/n3fit/src/n3fit/vpinterface.py index f851aafb3f..373515b3d4 100644 --- a/n3fit/src/n3fit/vpinterface.py +++ b/n3fit/src/n3fit/vpinterface.py @@ -205,7 +205,7 @@ def get_prefactor_factors(self, replica=None): if replica is None: replica = 1 # Replicas start counting in 1 so: - prefactor_layers = self._models[replica - 1].get_layer_re(r"pdf_prefactor_\d") + prefactor_layers = self._models[replica - 1].get_layer_re(r"prefactor_\d") if len(prefactor_layers) > 1: # We really don't want to fail at this point, but print a warning at least... log.warning("More than one prefactor layer found within the model!") From 6bc651fb52dac50b1f21d7937d6ce51348c7a41b Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 17 May 2023 12:33:33 +0200 Subject: [PATCH 20/68] Revert all changes to MSR --- n3fit/src/n3fit/layers/msr_normalization.py | 97 +-------------------- n3fit/src/n3fit/model_gen.py | 8 +- n3fit/src/n3fit/msr.py | 93 ++++++++++++++++++++ 3 files changed, 101 insertions(+), 97 deletions(-) create mode 100644 n3fit/src/n3fit/msr.py diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 1c78f7877f..c31bf8fb41 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -1,6 +1,4 @@ -import numpy as np -from n3fit.layers import xDivide, xIntegrator -from n3fit.backends import MetaLayer, Lambda +from n3fit.backends import MetaLayer from n3fit.backends import operations as op GLUON_IDX = [[2]] @@ -18,9 +16,7 @@ class MSR_Normalization(MetaLayer): _msr_enabled = False _vsr_enabled = False - def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs): - self.nx = nx - self.scaler = scaler + def __init__(self, output_dim=14, mode="ALL", **kwargs): if mode == True or mode.upper() == "ALL": self._msr_enabled = True self._vsr_enabled = True @@ -41,22 +37,9 @@ def __init__(self, output_dim=14, mode="ALL", nx=int(2e3), scaler=None, **kwargs op.scatter_to_one, op_kwargs={"indices": idx, "output_dim": output_dim} ) - self._gen_integration_input() - # 2. Prepare the pdf for integration - # for that we need to multiply several flavours with 1/x - self.get_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ") - self.divide_by_x = xDivide() - x_original = self.get_original(self.xgrid_integration) - self.x_divided = self.divide_by_x(x_original) - # 3. Now create the integration layer (the layer that will simply integrate, given some weight - self.integrator = xIntegrator(self.weights_array, input_shape=(self.nx,)) + super().__init__(**kwargs, name="normalizer") - self.compute_integrand = Lambda(op.op_multiply, name="pdf_integrand") - self.compute_normalized_pdf = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized") - - super().__init__(**kwargs, name="msr_weights") - - def call(self, pdf_integrand): + def call(self, pdf_integrated): """Imposes the valence and momentum sum rules: A_g = (1-sigma)/g A_v = A_v24 = A_v35 = 3/V @@ -66,7 +49,6 @@ def call(self, pdf_integrand): Note that both the input and the output are in the 14-flavours fk-basis """ - pdf_integrated = self.integrator(pdf_integrand) y = op.flatten(pdf_integrated) norm_constants = [] @@ -82,74 +64,3 @@ def call(self, pdf_integrand): norm_constants += n_av + n_av3 + n_av8 + n_av15 return self._out_scatter(norm_constants) - - def msr_impose(self): - """ - This function receives: - Generates a function that applies a normalization layer to the fit. - - fit_layer: the 8-basis layer of PDF which we fit - The normalization is computed from the direct output of the NN (so the 7,8-flavours basis) - - final_layer: the 14-basis which is fed to the fktable - and it is applied to the input of the fktable (i.e., to the 14-flavours fk-basis). - It uses pdf_fit to compute the sum rule and returns a modified version of - the final_pdf layer with a normalisation by which the sum rule is imposed - - Parameters - ---------- - scaler: scaler - Function to apply to the input. If given the input to the model - will be a (1, None, 2) tensor where dim [:,:,0] is scaled - """ - - # 4. Now create the normalization by selecting the right integrations - - # Finally prepare a function which will take as input the output of the PDF model - # and will return it appropiately normalized. - def apply_normalization(layer_pdf): - """ - layer_pdf: output of the PDF, unnormalized, ready for the fktable - """ - pdf_xgrid_integration = layer_pdf(self.xgrid_integration) - - def ultimate_pdf(x): - pdf_integrand = self.compute_integrand([self.x_divided, pdf_xgrid_integration]) - normalization_factor = self(pdf_integrand) - - pdf_xgrid = layer_pdf(x) - pdf_normalized = self.compute_normalized_pdf([pdf_xgrid, normalization_factor]) - return pdf_normalized - - return ultimate_pdf - - return apply_normalization - - def _gen_integration_input(self): - """ - Generates a np.array (shaped (nx,1)) of nx elements where the - nx/2 first elements are a logspace between 0 and 0.1 - and the rest a linspace from 0.1 to 0 - """ - lognx = int(self.nx / 2) - linnx = int(self.nx - lognx) - xgrid_log = np.logspace(-9, -1, lognx + 1) - xgrid_lin = np.linspace(0.1, 1, linnx) - xgrid = np.concatenate([xgrid_log[:-1], xgrid_lin]).reshape(self.nx, 1) - - spacing = [0.0] - for i in range(1, self.nx): - spacing.append(np.abs(xgrid[i - 1] - xgrid[i])) - spacing.append(0.0) - - weights = [] - for i in range(self.nx): - weights.append((spacing[i] + spacing[i + 1]) / 2.0) - self.weights_array = np.array(weights).reshape(self.nx, 1) - - # 1. Generate the fake input which will be used to integrate - # 1b If a scaler is provided, scale the input xgrid - if self.scaler: - xgrid = self.scaler(xgrid) - - # 5. Make the xgrid array into a backend input layer so it can be given to the normalization - self.xgrid_integration = op.numpy_to_input(xgrid, name="integration_grid") - diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index f7b7236025..5ad0fea1a5 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -12,8 +12,9 @@ from typing import List from dataclasses import dataclass import numpy as np +from n3fit.msr import msr_impose from n3fit.layers import DIS, DY, ObsRotation, losses -from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution, Mask, MSR_Normalization +from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution, Mask from n3fit.layers.observable import is_unique from n3fit.backends import MetaModel, Input @@ -574,9 +575,8 @@ def pdfNN_layer_generator( # Normalization and sum rules if impose_sumrule: - msr_normalization = MSR_Normalization(mode=impose_sumrule) - sumrule_layer = msr_normalization.msr_impose() - model_input["integrator_input"] = msr_normalization.xgrid_integration + sumrule_layer, integrator_input = msr_impose(mode=impose_sumrule, scaler=scaler) + model_input["integrator_input"] = integrator_input else: sumrule_layer = lambda x: x diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py new file mode 100644 index 0000000000..e99006de47 --- /dev/null +++ b/n3fit/src/n3fit/msr.py @@ -0,0 +1,93 @@ +""" + The constraint module include functions to impose the momentum sum rules on the PDFs +""" +import logging +import numpy as np + +from n3fit.layers import xDivide, MSR_Normalization, xIntegrator +from n3fit.backends import operations as op + + +log = logging.getLogger(__name__) + + +def gen_integration_input(nx): + """ + Generates a np.array (shaped (nx,1)) of nx elements where the + nx/2 first elements are a logspace between 0 and 0.1 + and the rest a linspace from 0.1 to 0 + """ + lognx = int(nx / 2) + linnx = int(nx - lognx) + xgrid_log = np.logspace(-9, -1, lognx + 1) + xgrid_lin = np.linspace(0.1, 1, linnx) + xgrid = np.concatenate([xgrid_log[:-1], xgrid_lin]).reshape(nx, 1) + + spacing = [0.0] + for i in range(1, nx): + spacing.append(np.abs(xgrid[i - 1] - xgrid[i])) + spacing.append(0.0) + + weights = [] + for i in range(nx): + weights.append((spacing[i] + spacing[i + 1]) / 2.0) + weights_array = np.array(weights).reshape(nx, 1) + + return xgrid, weights_array + + +def msr_impose(nx=int(2e3), mode='All', scaler=None): + """ + This function receives: + Generates a function that applies a normalization layer to the fit. + - fit_layer: the 8-basis layer of PDF which we fit + The normalization is computed from the direct output of the NN (so the 7,8-flavours basis) + - final_layer: the 14-basis which is fed to the fktable + and it is applied to the input of the fktable (i.e., to the 14-flavours fk-basis). + It uses pdf_fit to compute the sum rule and returns a modified version of + the final_pdf layer with a normalisation by which the sum rule is imposed + + Parameters + ---------- + nx: int + number of points for the integration grid, default: 2000 + mode: str + what sum rules to compute (MSR, VSR or All), default: All + scaler: scaler + Function to apply to the input. If given the input to the model + will be a (1, None, 2) tensor where dim [:,:,0] is scaled + """ + + # 1. Generate the fake input which will be used to integrate + xgrid, weights_array = gen_integration_input(nx) + # 1b If a scaler is provided, scale the input xgrid + if scaler: + xgrid = scaler(xgrid) + + # 2. Prepare the pdf for integration + # for that we need to multiply several flavours with 1/x + division_by_x = xDivide() + # 3. Now create the integration layer (the layer that will simply integrate, given some weight + integrator = xIntegrator(weights_array, input_shape=(nx,)) + + # 4. Now create the normalization by selecting the right integrations + normalizer = MSR_Normalization(mode=mode) + + # 5. Make the xgrid array into a backend input layer so it can be given to the normalization + xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") + # Finally prepare a function which will take as input the output of the PDF model + # and will return it appropiately normalized. + def apply_normalization(layer_pdf): + """ + layer_pdf: output of the PDF, unnormalized, ready for the fktable + """ + x_original = op.op_gather_keep_dims(xgrid_input, -1, axis=-1) + pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(xgrid_input)]) + normalization = normalizer(integrator(pdf_integrand)) + + def ultimate_pdf(x): + return layer_pdf(x)*normalization + + return ultimate_pdf + + return apply_normalization, xgrid_input From 96328a2eb77928d6f3fa22afeeacfdc2401d6504 Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 17 May 2023 13:54:32 +0200 Subject: [PATCH 21/68] Rewrite msr into a model that takes as inputs the pdf, pdf on integration grid and the integration grid, and outputs normalized pdf --- n3fit/src/n3fit/layers/msr_normalization.py | 2 +- n3fit/src/n3fit/model_gen.py | 11 +- n3fit/src/n3fit/msr.py | 133 +++++++++++--------- 3 files changed, 84 insertions(+), 62 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index c31bf8fb41..39ac5ac984 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -37,7 +37,7 @@ def __init__(self, output_dim=14, mode="ALL", **kwargs): op.scatter_to_one, op_kwargs={"indices": idx, "output_dim": output_dim} ) - super().__init__(**kwargs, name="normalizer") + super().__init__(**kwargs) def call(self, pdf_integrated): """Imposes the valence and momentum sum rules: diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 5ad0fea1a5..a67cb495c0 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -12,7 +12,7 @@ from typing import List from dataclasses import dataclass import numpy as np -from n3fit.msr import msr_impose +from n3fit.msr import generate_msr_model_and_grid from n3fit.layers import DIS, DY, ObsRotation, losses from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution, Mask from n3fit.layers.observable import is_unique @@ -575,7 +575,7 @@ def pdfNN_layer_generator( # Normalization and sum rules if impose_sumrule: - sumrule_layer, integrator_input = msr_impose(mode=impose_sumrule, scaler=scaler) + sumrule_layer, integrator_input = generate_msr_model_and_grid(mode=impose_sumrule, scaler=scaler) model_input["integrator_input"] = integrator_input else: sumrule_layer = lambda x: x @@ -621,10 +621,11 @@ def layer_fitbasis(x): def layer_pdf(x): return layer_evln(layer_fitbasis(x)) - # Final PDF (apply normalization) - final_pdf = sumrule_layer(layer_pdf) + pdf_unnormalized = layer_pdf(placeholder_input) + pdf_integration_grid = layer_pdf(integrator_input) - model_output = final_pdf(placeholder_input) + pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input]) + model_output = pdf_normalized # Create the model pdf_model = MetaModel(model_input, model_output, name=f"PDF_{i_replica}", scaler=scaler) diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index e99006de47..00881b49d8 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -7,10 +7,85 @@ from n3fit.layers import xDivide, MSR_Normalization, xIntegrator from n3fit.backends import operations as op +from n3fit.backends import MetaModel, Lambda, Input + log = logging.getLogger(__name__) +def generate_msr_model_and_grid( + output_dim: int = 14, + mode: str = "ALL", + nx: int = int(2e3), + scaler=None, + **kwargs) -> MetaModel: + """ + Generates a model that applies the sum rules to the PDF. + + Parameters + ---------- + output_dim: int + Number of flavours of the output PDF + mode: str + Mode of sum rules to apply. It can be: + - "ALL": applies both the momentum and valence sum rules + - "MSR": applies only the momentum sum rule + - "VSR": applies only the valence sum rule + nx: int + Number of points of the integration grid + scaler: Scaler + Scaler to be applied to the PDF before applying the sum rules + + Returns + ------- + model: MetaModel + Model that applies the sum rules to the PDF + It takes as inputs: + - pdf_x: the PDF output of the model + - pdf_xgrid_integration: the PDF output of the model evaluated at the integration grid + - xgrid_integration: the integration grid + It returns the PDF with the sum rules applied + xgrid_integration: dict + Dictionary with the integration grid, with: + - values: the integration grid + - input: the input layer of the integration grid + """ + # 0. Prepare input layers to MSR model + pdf_x = Input(shape=(None, output_dim), batch_size=1, name="pdf_x") + pdf_xgrid_integration = Input(shape=(nx, output_dim), batch_size=1, name="pdf_xgrid_integration") + + # 1. Generate the grid and weights that will be used to integrate + xgrid_integration, weights_array = gen_integration_input(nx) + # 1b If a scaler is provided, scale the input xgrid + if scaler: + xgrid_integration = scaler(xgrid_integration) + # 1c Get the original grid + x_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ")(xgrid_integration) + + # 2. Divide the grid by x depending on the flavour + x_divided = xDivide()(x_original) + + # 3. Prepare the pdf for integration by dividing by x + pdf_integrand = Lambda(op.op_multiply, name="pdf_integrand")([x_divided, pdf_xgrid_integration]) + + # 4. Integrate the pdf + pdf_integrated = xIntegrator(weights_array, input_shape=(nx,))(pdf_integrand) + + # 5. Compute the normalization factor + normalization_factor = MSR_Normalization(output_dim, mode, name="msr_weights")(pdf_integrated) + + # 6. Apply the normalization factor to the pdf + pdf_normalized = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized")([pdf_x, normalization_factor]) + + inputs = { + "pdf_x": pdf_x, + "pdf_xgrid_integration": pdf_xgrid_integration, + "xgrid_integration": xgrid_integration, + } + model = MetaModel(inputs, pdf_normalized, name="impose_msr") + + return model, xgrid_integration + def gen_integration_input(nx): """ Generates a np.array (shaped (nx,1)) of nx elements where the @@ -33,61 +108,7 @@ def gen_integration_input(nx): weights.append((spacing[i] + spacing[i + 1]) / 2.0) weights_array = np.array(weights).reshape(nx, 1) - return xgrid, weights_array - + xgrid = op.numpy_to_input(xgrid, name="integration_grid") -def msr_impose(nx=int(2e3), mode='All', scaler=None): - """ - This function receives: - Generates a function that applies a normalization layer to the fit. - - fit_layer: the 8-basis layer of PDF which we fit - The normalization is computed from the direct output of the NN (so the 7,8-flavours basis) - - final_layer: the 14-basis which is fed to the fktable - and it is applied to the input of the fktable (i.e., to the 14-flavours fk-basis). - It uses pdf_fit to compute the sum rule and returns a modified version of - the final_pdf layer with a normalisation by which the sum rule is imposed - - Parameters - ---------- - nx: int - number of points for the integration grid, default: 2000 - mode: str - what sum rules to compute (MSR, VSR or All), default: All - scaler: scaler - Function to apply to the input. If given the input to the model - will be a (1, None, 2) tensor where dim [:,:,0] is scaled - """ + return xgrid, weights_array - # 1. Generate the fake input which will be used to integrate - xgrid, weights_array = gen_integration_input(nx) - # 1b If a scaler is provided, scale the input xgrid - if scaler: - xgrid = scaler(xgrid) - - # 2. Prepare the pdf for integration - # for that we need to multiply several flavours with 1/x - division_by_x = xDivide() - # 3. Now create the integration layer (the layer that will simply integrate, given some weight - integrator = xIntegrator(weights_array, input_shape=(nx,)) - - # 4. Now create the normalization by selecting the right integrations - normalizer = MSR_Normalization(mode=mode) - - # 5. Make the xgrid array into a backend input layer so it can be given to the normalization - xgrid_input = op.numpy_to_input(xgrid, name="integration_grid") - # Finally prepare a function which will take as input the output of the PDF model - # and will return it appropiately normalized. - def apply_normalization(layer_pdf): - """ - layer_pdf: output of the PDF, unnormalized, ready for the fktable - """ - x_original = op.op_gather_keep_dims(xgrid_input, -1, axis=-1) - pdf_integrand = op.op_multiply([division_by_x(x_original), layer_pdf(xgrid_input)]) - normalization = normalizer(integrator(pdf_integrand)) - - def ultimate_pdf(x): - return layer_pdf(x)*normalization - - return ultimate_pdf - - return apply_normalization, xgrid_input From 82cf95b47686f104776ee5d4934d0cbadfc47994 Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 17 May 2023 14:04:01 +0200 Subject: [PATCH 22/68] Set shape of integration grid to (None, 1) rather than (2000, 1) to display shapes in model summary and plot --- n3fit/src/n3fit/backends/keras_backend/operations.py | 6 +++++- n3fit/src/n3fit/msr.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 8cf1065849..e348649481 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -112,7 +112,7 @@ def batchit(x, batch_dimension=0, **kwarg): # layer generation -def numpy_to_input(numpy_array, no_reshape=False, name=None): +def numpy_to_input(numpy_array, no_reshape=False, name=None, custom_shape: tuple = None): """ Takes a numpy array and generates a Input layer. By default it adds a batch dimension (of size 1) so that the shape of the layer @@ -125,6 +125,8 @@ def numpy_to_input(numpy_array, no_reshape=False, name=None): if true, don't add batch dimension, take the first dimension of the array as the batch name: bool name to give to the layer + custom_shape: tuple + To specify a more general shape with None values """ if no_reshape: batched_array = numpy_array @@ -134,6 +136,8 @@ def numpy_to_input(numpy_array, no_reshape=False, name=None): batched_array = np.expand_dims(numpy_array, 0) batch_size = 1 shape = numpy_array.shape + if custom_shape is not None: + shape = custom_shape input_layer = Input(batch_size=batch_size, shape=shape, name=name) input_layer.tensor_content = batched_array input_layer.original_shape = no_reshape diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index 00881b49d8..fd769ae737 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -108,7 +108,8 @@ def gen_integration_input(nx): weights.append((spacing[i] + spacing[i + 1]) / 2.0) weights_array = np.array(weights).reshape(nx, 1) - xgrid = op.numpy_to_input(xgrid, name="integration_grid") + # Specify a custom shape here using None, so shapes will display properly in the model summary + xgrid = op.numpy_to_input(xgrid, name="integration_grid", custom_shape=(None, 1)) return xgrid, weights_array From 7b475bafefb4c3ca4400fe7223635e9d57c3d3b6 Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 17 May 2023 14:59:57 +0200 Subject: [PATCH 23/68] Revert renaming of Preprocessing to Prefactor (now use PreprocessingFactor for clarity and consistency) --- n3fit/src/n3fit/io/writer.py | 2 +- n3fit/src/n3fit/layers/__init__.py | 2 +- .../layers/{prefactor.py => preprocessing.py} | 4 ++-- n3fit/src/n3fit/model_gen.py | 8 +++---- n3fit/src/n3fit/vpinterface.py | 22 +++++++++---------- 5 files changed, 19 insertions(+), 19 deletions(-) rename n3fit/src/n3fit/layers/{prefactor.py => preprocessing.py} (97%) diff --git a/n3fit/src/n3fit/io/writer.py b/n3fit/src/n3fit/io/writer.py index 32418e9543..6b4879f5a8 100644 --- a/n3fit/src/n3fit/io/writer.py +++ b/n3fit/src/n3fit/io/writer.py @@ -318,7 +318,7 @@ def jsonfit(replica_status, pdf_object, tr_chi2, vl_chi2, true_chi2, stop_epoch, """ all_info = {} # Generate preprocessing information - all_info["preprocessing"] = pdf_object.get_prefactor_factors() + all_info["preprocessing"] = pdf_object.get_preprocessing_factors() # .fitinfo-like info all_info["stop_epoch"] = stop_epoch all_info["best_epoch"] = replica_status.best_epoch diff --git a/n3fit/src/n3fit/layers/__init__.py b/n3fit/src/n3fit/layers/__init__.py index ccc78fbb15..c8a7c6fafb 100644 --- a/n3fit/src/n3fit/layers/__init__.py +++ b/n3fit/src/n3fit/layers/__init__.py @@ -1,4 +1,4 @@ -from .prefactor import Prefactor +from .preprocessing import PreprocessingFactor from .rotations import FkRotation, FlavourToEvolution, ObsRotation from .x_operations import xIntegrator, xDivide from .msr_normalization import MSR_Normalization diff --git a/n3fit/src/n3fit/layers/prefactor.py b/n3fit/src/n3fit/layers/preprocessing.py similarity index 97% rename from n3fit/src/n3fit/layers/prefactor.py rename to n3fit/src/n3fit/layers/preprocessing.py index 9651c059a1..d312483874 100644 --- a/n3fit/src/n3fit/layers/prefactor.py +++ b/n3fit/src/n3fit/layers/preprocessing.py @@ -3,7 +3,7 @@ from n3fit.backends import operations as op -class Prefactor(MetaLayer): +class PreprocessingFactor(MetaLayer): """ Computes prefactor for the PDF. @@ -105,7 +105,7 @@ def build(self, input_shape): beta_name = f"beta_{flav_name}" self.generate_weight(beta_name, "largex", flav_dict, set_to_zero=not self.large_x) - super(Prefactor, self).build(input_shape) + super(PreprocessingFactor, self).build(input_shape) def call(self, inputs, **kwargs): x = inputs diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index a67cb495c0..db2f426b51 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -14,7 +14,7 @@ import numpy as np from n3fit.msr import generate_msr_model_and_grid from n3fit.layers import DIS, DY, ObsRotation, losses -from n3fit.layers import Preprocessing, FkRotation, FlavourToEvolution, Mask +from n3fit.layers import PreprocessingFactor, FkRotation, FlavourToEvolution, Mask from n3fit.layers.observable import is_unique from n3fit.backends import MetaModel, Input @@ -482,7 +482,7 @@ def pdfNN_layer_generator( selects the type of architecture of the NN. Default: dense flav_info: dict dictionary containing the information about each PDF (basis dictionary in the runcard) - to be used by Prefactor + to be used by PreprocessingFactor out: int number of output flavours of the model (default 14) seed: list(int) @@ -584,10 +584,10 @@ def pdfNN_layer_generator( pdf_models = [] for i_replica, replica_seed in enumerate(seed): - compute_prefactor = Prefactor( + compute_prefactor = PreprocessingFactor( flav_info=flav_info, input_shape=(1,), - name=f"prefactor_{i_replica}", + name=f"preprocessing_factor_{i_replica}", seed=replica_seed + number_of_layers, large_x=not subtract_one, ) diff --git a/n3fit/src/n3fit/vpinterface.py b/n3fit/src/n3fit/vpinterface.py index 373515b3d4..1986c96dad 100644 --- a/n3fit/src/n3fit/vpinterface.py +++ b/n3fit/src/n3fit/vpinterface.py @@ -195,31 +195,31 @@ def get_nn_weights(self): """Outputs all weights of the NN as numpy.ndarrays""" return [model.get_weights() for model in self._models] - def get_prefactor_factors(self, replica=None): - """Loads the prefactor alpha and beta arrays from the PDF trained model. + def get_preprocessing_factors(self, replica=None): + """Loads the preprocessing alpha and beta arrays from the PDF trained model. If a ``fit_basis`` given in the format of ``n3fit`` runcards is given it will be used to generate a new dictionary with the names, the exponent and whether they are trainable otherwise outputs a Nx2 array where [:,0] are alphas and [:,1] betas """ - # If no replica is explicitly requested, get the prefactor layer for the first model + # If no replica is explicitly requested, get the preprocessing layer for the first model if replica is None: replica = 1 # Replicas start counting in 1 so: - prefactor_layers = self._models[replica - 1].get_layer_re(r"prefactor_\d") - if len(prefactor_layers) > 1: + preprocessing_layers = self._models[replica - 1].get_layer_re(r"preprocessing_factor_\d") + if len(preprocessing_layers) > 1: # We really don't want to fail at this point, but print a warning at least... - log.warning("More than one prefactor layer found within the model!") - elif len(prefactor_layers) < 1: - log.warning("No prefactor layer found within the model!") - prefactor_layer = prefactor_layers[0] + log.warning("More than one preprocessing layer found within the model!") + elif len(preprocessing_layers) < 1: + log.warning("No preprocessing layer found within the model!") + preprocessing_layer = preprocessing_layers[0] alphas_and_betas = None if self.fit_basis is not None: output_dictionaries = [] for d in self.fit_basis: flavour = d["fl"] - alpha = prefactor_layer.get_weight_by_name(f"alpha_{flavour}") - beta = prefactor_layer.get_weight_by_name(f"beta_{flavour}") + alpha = preprocessing_layer.get_weight_by_name(f"alpha_{flavour}") + beta = preprocessing_layer.get_weight_by_name(f"beta_{flavour}") if alpha is not None: alpha = float(alpha.numpy()) if beta is not None: From 7153e939a77fd66c23854646b20829b4023aa20e Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 17 May 2023 15:25:37 +0200 Subject: [PATCH 24/68] Clean up model creation code --- n3fit/src/n3fit/model_gen.py | 91 +++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index db2f426b51..5bc72fd35a 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -582,47 +582,61 @@ def pdfNN_layer_generator( # Now we need a trainable network per replica to be trained in parallel pdf_models = [] - for i_replica, replica_seed in enumerate(seed): - compute_prefactor = PreprocessingFactor( - flav_info=flav_info, - input_shape=(1,), - name=f"preprocessing_factor_{i_replica}", - seed=replica_seed + number_of_layers, - large_x=not subtract_one, + # Only these layers change from replica to replica: + nn_replicas = [] + prefactor_replicas = [] + for i_replica, replica_seed in enumerate(seed): + prefactor_replicas.append( + PreprocessingFactor( + flav_info=flav_info, + input_shape=(1,), + name=f"preprocessing_factor_{i_replica}", + seed=replica_seed + number_of_layers, + large_x=not subtract_one, + ) ) + nn_replicas.append( + generate_nn( + layer_type, inp, nodes, activations, initializer_name, + replica_seed, dropout, regularizer, regularizer_args, + last_layer_nodes, name=f"NN_{i_replica}") + ) + + # All layers have been made, now we need to connect them, + # do this in a function so we can call it for both grids and each replica + # Since all layers are already made, they will be reused + def compute_unnormalized_pdf(x, neural_network, compute_prefactor): + # Preprocess the input grid + x_scaled = extract_scaled(x) + x_original = extract_original(x) + x_processed = process_input(x_scaled) + + # Compute the neural network output + nn_output = neural_network(x_processed) + if subtract_one: + x_eq_1_processed = process_input(layer_x_eq_1) + nn_at_one = neural_network(x_eq_1_processed) + nn_output = subtract_one_layer([nn_output, nn_at_one]) - neural_network = generate_nn( - layer_type, inp, nodes, activations, initializer_name, - replica_seed, dropout, regularizer, regularizer_args, - last_layer_nodes, name=f"NN_{i_replica}") - - # Apply preprocessing and basis - def layer_fitbasis(x): - """The tensor x has a expected shape of (1, None, {1,2}) - where x[...,0] corresponds to the feature_scaled input and x[...,-1] the original input - """ - x_scaled = extract_scaled(x) - x_original = extract_original(x) - - nn_output = neural_network(process_input(x_scaled)) - if subtract_one: - nn_at_one = neural_network(process_input(layer_x_eq_1)) - nn_output = subtract_one_layer([nn_output, nn_at_one]) - - prefactor = compute_prefactor(x_original) - ret = apply_prefactor([nn_output, prefactor]) - if not basis_rotation.is_identity(): - # if we don't need to rotate basis we don't want spurious layers - ret = basis_rotation(ret) - return ret - - # Rotation layer, changes from the 8-basis to the 14-basis - def layer_pdf(x): - return layer_evln(layer_fitbasis(x)) - - pdf_unnormalized = layer_pdf(placeholder_input) - pdf_integration_grid = layer_pdf(integrator_input) + # Compute the preprocessing prefactor and multiply + prefactor = compute_prefactor(x_original) + pref_nn = apply_prefactor([nn_output, prefactor]) + + # Apply basis rotation if needed + if not basis_rotation.is_identity(): + pref_nn = basis_rotation(pref_nn) + + # Transform to FK basis + pdf_unnormalized = layer_evln(pref_nn) + + return pdf_unnormalized + + # Finally compute the normalized PDFs for each replica + pdf_models = [] + for i_replica, (prefactor, nn) in enumerate(zip(prefactor_replicas, nn_replicas)): + pdf_unnormalized = compute_unnormalized_pdf(placeholder_input, nn, prefactor) + pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, prefactor) pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input]) model_output = pdf_normalized @@ -630,6 +644,7 @@ def layer_pdf(x): # Create the model pdf_model = MetaModel(model_input, model_output, name=f"PDF_{i_replica}", scaler=scaler) pdf_models.append(pdf_model) + return pdf_models def generate_nn( From dfb4eed6536b1d4d4849446cf5524f3649d6e788 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 22 May 2023 13:47:00 +0200 Subject: [PATCH 25/68] Reorganize input options, keeping effect the same --- n3fit/src/n3fit/model_gen.py | 70 ++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 5bc72fd35a..8bb5f2545a 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -515,9 +515,6 @@ def pdfNN_layer_generator( if impose_sumrule is None: impose_sumrule = "All" - if scaler: - inp = 1 - if activations is None: activations = ["tanh", "linear"] elif callable(activations): @@ -532,41 +529,50 @@ def pdfNN_layer_generator( # The number of nodes in the last layer is equal to the number of fitted flavours last_layer_nodes = nodes[-1] # (== len(flav_info)) - # Generate the generic layers that will not depend on extra considerations - - # First prepare the input for the PDF model and any scaling if needed - placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') - - subtract_one = False - process_input = Lambda(lambda x: x, name='process_input') - input_x_eq_1 = [1.0] + # Process input options. There are 3 options: + # 1. Do nothing + # 2. Scale the input + # 3. Concatenate log(x) to the input + # when feature scaling is on, don't add logs regardless of the input + use_feature_scaling = scaler is not None + add_logs = inp == 2 and not use_feature_scaling # When scaler is active we also want to do the subtraction of large x # TODO: make it its own option (i.e., one could want to use this without using scaler) - if scaler: - # change the input domain [0,1] -> [-1,1] - process_input = Lambda(lambda x: 2 * x - 1, name='process_input') - subtract_one = True - input_x_eq_1 = scaler([1.0])[0] - placeholder_input = Input(shape=(None, 2), batch_size=1, name='x') - elif inp == 2: - # If the input is of type (x, logx) - # create a x --> (x, logx) layer to preppend to everything - process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='x_logx') + subtract_one = use_feature_scaling - extract_scaled = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') - extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') - - # the layer that multiplies the NN output by the prefactor - apply_prefactor = Lambda(op.op_multiply, name='prefactor_times_NN') + if use_feature_scaling: + inp = 1 - # the layer that subtracts 1 from the NN output - subtract_one_layer = Lambda(op.op_subtract, name='subtract_one') + # Define the main input + if add_logs: + placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') + process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='x_logx') + elif use_feature_scaling: + # Note feature scaling happens before the model created here, + # so the input is of the form (scaler(x), x) + placeholder_input = Input(shape=(None, 2), batch_size=1, name='scaledx_x') + process_input = Lambda(lambda x: 2 * x - 1, name='process_input') + else: + placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') + process_input = None model_input = {"pdf_input": placeholder_input} + if subtract_one: + input_x_eq_1 = [1.0] + if use_feature_scaling: + input_x_eq_1 = scaler(input_x_eq_1)[0] + # the layer that subtracts 1 from the NN output + subtract_one_layer = Lambda(op.op_subtract, name='subtract_one') layer_x_eq_1 = op.numpy_to_input(np.array(input_x_eq_1).reshape(1, 1)) model_input["layer_x_eq_1"] = layer_x_eq_1 + extract_scaled = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') + extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') + + # the layer that multiplies the NN output by the prefactor + apply_prefactor = Lambda(op.op_multiply, name='prefactor_times_NN') + # Basis rotation basis_rotation = FlavourToEvolution(flav_info=flav_info, fitbasis=fitbasis, name="pdf_evolution_basis") @@ -610,7 +616,10 @@ def compute_unnormalized_pdf(x, neural_network, compute_prefactor): # Preprocess the input grid x_scaled = extract_scaled(x) x_original = extract_original(x) - x_processed = process_input(x_scaled) + if process_input is not None: + x_processed = process_input(x_scaled) + else: + x_processed = x_scaled # Compute the neural network output nn_output = neural_network(x_processed) @@ -672,7 +681,8 @@ def generate_nn( # Note: using a Sequential model would be more appropriate, but it would require # creating a MetaSequential model. - x = Input(shape=(None, inp), batch_size=1, name='xgrids_processed') + input_dimensions = 2 if inp == 2 or scaler is not None else 1 + x = Input(shape=(None, input_dimensions), batch_size=1, name='xgrids_processed') pdf = x for layer in list_of_pdf_layers: pdf = layer(pdf) From 9dc1384ba8d67cf020b3287ece1ae5ee97f4c9f6 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 22 May 2023 14:17:41 +0200 Subject: [PATCH 26/68] Make preprocessing layers dependent on which option is used (scaling, adding logs) --- n3fit/src/n3fit/model_gen.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 8bb5f2545a..410362719f 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -536,6 +536,7 @@ def pdfNN_layer_generator( # when feature scaling is on, don't add logs regardless of the input use_feature_scaling = scaler is not None add_logs = inp == 2 and not use_feature_scaling + print(f"******************** inp: {inp}, use_feature_scaling: {use_feature_scaling}, add_logs: {add_logs}") # When scaler is active we also want to do the subtraction of large x # TODO: make it its own option (i.e., one could want to use this without using scaler) subtract_one = use_feature_scaling @@ -547,14 +548,20 @@ def pdfNN_layer_generator( if add_logs: placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='x_logx') + extract_original = None + extract_nn_input = None elif use_feature_scaling: # Note feature scaling happens before the model created here, # so the input is of the form (scaler(x), x) placeholder_input = Input(shape=(None, 2), batch_size=1, name='scaledx_x') process_input = Lambda(lambda x: 2 * x - 1, name='process_input') + extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') + extract_nn_input = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') else: placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') process_input = None + extract_original = None + extract_nn_input = None model_input = {"pdf_input": placeholder_input} @@ -567,8 +574,6 @@ def pdfNN_layer_generator( layer_x_eq_1 = op.numpy_to_input(np.array(input_x_eq_1).reshape(1, 1)) model_input["layer_x_eq_1"] = layer_x_eq_1 - extract_scaled = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') - extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') # the layer that multiplies the NN output by the prefactor apply_prefactor = Lambda(op.op_multiply, name='prefactor_times_NN') @@ -614,12 +619,9 @@ def pdfNN_layer_generator( # Since all layers are already made, they will be reused def compute_unnormalized_pdf(x, neural_network, compute_prefactor): # Preprocess the input grid - x_scaled = extract_scaled(x) - x_original = extract_original(x) - if process_input is not None: - x_processed = process_input(x_scaled) - else: - x_processed = x_scaled + x_nn_input = extract_nn_input(x) if extract_nn_input is not None else x + x_original = extract_original(x) if extract_original is not None else x + x_processed = process_input(x_nn_input) if process_input is not None else x_nn_input # Compute the neural network output nn_output = neural_network(x_processed) From 7c6d8ae340fa71e7e1c5e15c69bcc55f59cac0d8 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 22 May 2023 14:33:44 +0200 Subject: [PATCH 27/68] Fix msr with scaling --- n3fit/src/n3fit/model_gen.py | 19 +++++++++++++------ n3fit/src/n3fit/msr.py | 9 ++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 410362719f..19e841ae6f 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -609,10 +609,18 @@ def pdfNN_layer_generator( ) nn_replicas.append( generate_nn( - layer_type, inp, nodes, activations, initializer_name, - replica_seed, dropout, regularizer, regularizer_args, - last_layer_nodes, name=f"NN_{i_replica}") - ) + layer_type=layer_type, + inp=inp, + nodes=nodes, + activations=activations, + initializer_name=initializer_name, + replica_seed=replica_seed, + dropout=dropout, + regularizer=regularizer, + regularizer_args=regularizer_args, + last_layer_nodes=last_layer_nodes, + name=f"NN_{i_replica}") + ) # All layers have been made, now we need to connect them, # do this in a function so we can call it for both grids and each replica @@ -683,8 +691,7 @@ def generate_nn( # Note: using a Sequential model would be more appropriate, but it would require # creating a MetaSequential model. - input_dimensions = 2 if inp == 2 or scaler is not None else 1 - x = Input(shape=(None, input_dimensions), batch_size=1, name='xgrids_processed') + x = Input(shape=(None, inp), batch_size=1, name='xgrids_processed') pdf = x for layer in list_of_pdf_layers: pdf = layer(pdf) diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index fd769ae737..bb83baaedc 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -59,6 +59,12 @@ def generate_msr_model_and_grid( # 1b If a scaler is provided, scale the input xgrid if scaler: xgrid_integration = scaler(xgrid_integration) + + # Turn into input layer. Specify a custom shape here using None, + # so shapes will display properly in the model summary + xgrid_integration = op.numpy_to_input( + xgrid_integration, name="integration_grid", custom_shape=(None, 1)) + # 1c Get the original grid x_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ")(xgrid_integration) @@ -108,8 +114,5 @@ def gen_integration_input(nx): weights.append((spacing[i] + spacing[i + 1]) / 2.0) weights_array = np.array(weights).reshape(nx, 1) - # Specify a custom shape here using None, so shapes will display properly in the model summary - xgrid = op.numpy_to_input(xgrid, name="integration_grid", custom_shape=(None, 1)) - return xgrid, weights_array From cbce1813ba3e2ec08a35eea5595125074a200915 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 22 May 2023 14:54:20 +0200 Subject: [PATCH 28/68] Fix msr with scaling, shape of integration grid --- n3fit/src/n3fit/msr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index bb83baaedc..7ea45a68f3 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -62,8 +62,9 @@ def generate_msr_model_and_grid( # Turn into input layer. Specify a custom shape here using None, # so shapes will display properly in the model summary + grid_shape = 2 if scaler is not None else 1 xgrid_integration = op.numpy_to_input( - xgrid_integration, name="integration_grid", custom_shape=(None, 1)) + xgrid_integration, name="integration_grid", custom_shape=(None, grid_shape)) # 1c Get the original grid x_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ")(xgrid_integration) From 3296aa473a8f1c32cb78ca65fcd20a18eb527085 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 22 May 2023 14:54:52 +0200 Subject: [PATCH 29/68] Set shape of x=1 input to None so tensorflow displays proper shapes --- n3fit/src/n3fit/model_gen.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 19e841ae6f..7d29a0d20f 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -571,7 +571,10 @@ def pdfNN_layer_generator( input_x_eq_1 = scaler(input_x_eq_1)[0] # the layer that subtracts 1 from the NN output subtract_one_layer = Lambda(op.op_subtract, name='subtract_one') - layer_x_eq_1 = op.numpy_to_input(np.array(input_x_eq_1).reshape(1, 1)) + layer_x_eq_1 = op.numpy_to_input( + np.array(input_x_eq_1).reshape(1, 1), + name='x_ones', + custom_shape=(None, 1)) # Just to make shapes consistent model_input["layer_x_eq_1"] = layer_x_eq_1 From fba7827821201f3734e36b9cbfe747d11ce9c666 Mon Sep 17 00:00:00 2001 From: Aron Date: Thu, 25 May 2023 16:01:34 +0200 Subject: [PATCH 30/68] Add dummy ph_replica argument to MSR_Normalization layer --- n3fit/src/n3fit/msr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index 1c42b03d53..4de24c6012 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -86,7 +86,8 @@ def generate_msr_model_and_grid( photons_c = photons.integral # 6. Compute the normalization factor - normalization_factor = MSR_Normalization(output_dim, mode, name="msr_weights", photons_contribution=photons_c)(pdf_integrated) + # For now set the photon component to None + normalization_factor = MSR_Normalization(output_dim, mode, name="msr_weights", photons_contribution=photons_c)(pdf_integrated, ph_replica=None) # 7. Apply the normalization factor to the pdf pdf_normalized = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized")([pdf_x, normalization_factor]) From 38e1984d0fd3d7f2c71545d801ab05d201442551 Mon Sep 17 00:00:00 2001 From: Aron Date: Tue, 23 May 2023 13:16:37 +0200 Subject: [PATCH 31/68] Factor out scaler into own module scaler.py --- n3fit/src/n3fit/model_trainer.py | 50 ++------------------------ n3fit/src/n3fit/scaler.py | 62 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 48 deletions(-) create mode 100644 n3fit/src/n3fit/scaler.py diff --git a/n3fit/src/n3fit/model_trainer.py b/n3fit/src/n3fit/model_trainer.py index 1f1a5a4011..a9d3328966 100644 --- a/n3fit/src/n3fit/model_trainer.py +++ b/n3fit/src/n3fit/model_trainer.py @@ -13,10 +13,10 @@ import logging import numpy as np -from scipy.interpolate import PchipInterpolator from n3fit import model_gen from n3fit.backends import MetaModel, callbacks, clear_backend_state +from n3fit.scaler import generate_scaler from n3fit.backends import operations as op import n3fit.hyper_optimization.penalties import n3fit.hyper_optimization.rewards @@ -600,53 +600,7 @@ def _generate_observables( # Store a reference to the interpolator as self._scaler if interpolation_points: - input_arr = np.concatenate(self.input_list, axis=1) - input_arr = np.sort(input_arr) - input_arr_size = input_arr.size - - # Define an evenly spaced grid in the domain [0,1] - # force_set_smallest is used to make sure the smallest point included in the scaling is - # 1e-9, to prevent trouble when saving it to the LHAPDF grid - force_set_smallest = input_arr.min() > 1e-9 - if force_set_smallest: - new_xgrid = np.linspace( - start=1 / input_arr_size, stop=1.0, endpoint=False, num=input_arr_size - ) - else: - new_xgrid = np.linspace(start=0, stop=1.0, endpoint=False, num=input_arr_size) - - # When mapping the FK xgrids onto our new grid, we need to consider degeneracies among - # the x-values in the FK grids - unique, counts = np.unique(input_arr, return_counts=True) - map_to_complete = [] - for cumsum_ in np.cumsum(counts): - # Make sure to include the smallest new_xgrid value, such that we have a point at - # x<=1e-9 - map_to_complete.append(new_xgrid[cumsum_ - counts[0]]) - map_to_complete = np.array(map_to_complete) - map_from_complete = unique - - # If needed, set feature_scaling(x=1e-9)=0 - if force_set_smallest: - map_from_complete = np.insert(map_from_complete, 0, 1e-9) - map_to_complete = np.insert(map_to_complete, 0, 0.0) - - # Select the indices of the points that will be used by the interpolator - onein = map_from_complete.size / (int(interpolation_points) - 1) - selected_points = [round(i * onein - 1) for i in range(1, int(interpolation_points))] - if selected_points[0] != 0: - selected_points = [0] + selected_points - map_from = map_from_complete[selected_points] - map_from = np.log(map_from) - map_to = map_to_complete[selected_points] - - try: - scaler = PchipInterpolator(map_from, map_to) - except ValueError: - raise ValueError( - "interpolation_points is larger than the number of unique " "input x-values" - ) - self._scaler = lambda x: np.concatenate([scaler(np.log(x)), x], axis=-1) + self._scaler = generate_scaler(self.input_list, interpolation_points) def _generate_pdf( self, diff --git a/n3fit/src/n3fit/scaler.py b/n3fit/src/n3fit/scaler.py new file mode 100644 index 0000000000..056e53745d --- /dev/null +++ b/n3fit/src/n3fit/scaler.py @@ -0,0 +1,62 @@ +from typing import Callable, List +import numpy as np +from scipy.interpolate import PchipInterpolator + + +def generate_scaler(input_list: List[np.ndarray], interpolation_points: int = None) -> Callable: + """ + Generate the scaler function that applies feature scaling to the input data. + + Parameters + ---------- + input_list : list of numpy.ndarray + The list of input data arrays. + interpolation_points : int, optional + + Returns + ------- + _scaler : function + The scaler function that applies feature scaling to the input data. + """ + input_arr = np.concatenate(input_list, axis=1) + input_arr = np.sort(input_arr) + input_arr_size = input_arr.size + + force_set_smallest = input_arr.min() > 1e-9 + if force_set_smallest: + new_xgrid = np.linspace( + start=1 / input_arr_size, stop=1.0, endpoint=False, num=input_arr_size + ) + else: + new_xgrid = np.linspace(start=0, stop=1.0, endpoint=False, num=input_arr_size) + + unique, counts = np.unique(input_arr, return_counts=True) + map_to_complete = [] + for cumsum_ in np.cumsum(counts): + map_to_complete.append(new_xgrid[cumsum_ - counts[0]]) + map_to_complete = np.array(map_to_complete) + map_from_complete = unique + + if force_set_smallest: + map_from_complete = np.insert(map_from_complete, 0, 1e-9) + map_to_complete = np.insert(map_to_complete, 0, 0.0) + + onein = map_from_complete.size / (int(interpolation_points) - 1) + selected_points = [round(i * onein - 1) for i in range(1, int(interpolation_points))] + if selected_points[0] != 0: + selected_points = [0] + selected_points + map_from = map_from_complete[selected_points] + map_from = np.log(map_from) + map_to = map_to_complete[selected_points] + + try: + scaler = PchipInterpolator(map_from, map_to) + except ValueError: + raise ValueError( + "interpolation_points is larger than the number of unique input x-values" + ) + + _scaler = lambda x: np.concatenate([scaler(np.log(x)), x], axis=-1) + + return _scaler + From d0065df9762904f0d204cec7e1c6d7f624f5b4d9 Mon Sep 17 00:00:00 2001 From: Aron Date: Tue, 23 May 2023 15:31:36 +0200 Subject: [PATCH 32/68] Do scaling of [0,1] to [-1, 1] inside scaler --- n3fit/src/n3fit/model_gen.py | 8 +++++--- n3fit/src/n3fit/scaler.py | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index e752e04f39..e10a215d33 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -568,7 +568,7 @@ def pdfNN_layer_generator( # Note feature scaling happens before the model created here, # so the input is of the form (scaler(x), x) placeholder_input = Input(shape=(None, 2), batch_size=1, name='scaledx_x') - process_input = Lambda(lambda x: 2 * x - 1, name='process_input') + process_input = None extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') extract_nn_input = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') else: @@ -591,7 +591,6 @@ def pdfNN_layer_generator( custom_shape=(None, 1)) # Just to make shapes consistent model_input["layer_x_eq_1"] = layer_x_eq_1 - # the layer that multiplies the NN output by the prefactor apply_prefactor = Lambda(op.op_multiply, name='prefactor_times_NN') @@ -654,7 +653,10 @@ def compute_unnormalized_pdf(x, neural_network, compute_prefactor): # Compute the neural network output nn_output = neural_network(x_processed) if subtract_one: - x_eq_1_processed = process_input(layer_x_eq_1) + if process_input is not None: + x_eq_1_processed = process_input(layer_x_eq_1) + else: + x_eq_1_processed = layer_x_eq_1 nn_at_one = neural_network(x_eq_1_processed) nn_output = subtract_one_layer([nn_output, nn_at_one]) diff --git a/n3fit/src/n3fit/scaler.py b/n3fit/src/n3fit/scaler.py index 056e53745d..9eac8ed575 100644 --- a/n3fit/src/n3fit/scaler.py +++ b/n3fit/src/n3fit/scaler.py @@ -56,7 +56,10 @@ def generate_scaler(input_list: List[np.ndarray], interpolation_points: int = No "interpolation_points is larger than the number of unique input x-values" ) - _scaler = lambda x: np.concatenate([scaler(np.log(x)), x], axis=-1) + def _scaler(x): + x_scaled = scaler(np.log(x)) + x_scaled = 2 * x_scaled - 1 + return np.concatenate([x_scaled, x], axis=-1) return _scaler From 2d2f8a03939c8db0951b0c361a631bddc231510a Mon Sep 17 00:00:00 2001 From: Aron Date: Tue, 23 May 2023 15:37:17 +0200 Subject: [PATCH 33/68] Replace None layers with identity functions --- n3fit/src/n3fit/model_gen.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index e10a215d33..c24fc17f8c 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -559,23 +559,24 @@ def pdfNN_layer_generator( inp = 1 # Define the main input + do_nothing = lambda x: x if add_logs: placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='x_logx') - extract_original = None - extract_nn_input = None + extract_original = do_nothing + extract_nn_input = do_nothing elif use_feature_scaling: # Note feature scaling happens before the model created here, # so the input is of the form (scaler(x), x) placeholder_input = Input(shape=(None, 2), batch_size=1, name='scaledx_x') - process_input = None + process_input = do_nothing extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') extract_nn_input = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') else: placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') - process_input = None - extract_original = None - extract_nn_input = None + process_input = do_nothing + extract_original = do_nothing + extract_nn_input = do_nothing model_input = {"pdf_input": placeholder_input} @@ -646,17 +647,14 @@ def pdfNN_layer_generator( # Since all layers are already made, they will be reused def compute_unnormalized_pdf(x, neural_network, compute_prefactor): # Preprocess the input grid - x_nn_input = extract_nn_input(x) if extract_nn_input is not None else x - x_original = extract_original(x) if extract_original is not None else x - x_processed = process_input(x_nn_input) if process_input is not None else x_nn_input + x_nn_input = extract_nn_input(x) + x_original = extract_original(x) + x_processed = process_input(x_nn_input) # Compute the neural network output nn_output = neural_network(x_processed) if subtract_one: - if process_input is not None: - x_eq_1_processed = process_input(layer_x_eq_1) - else: - x_eq_1_processed = layer_x_eq_1 + x_eq_1_processed = process_input(layer_x_eq_1) nn_at_one = neural_network(x_eq_1_processed) nn_output = subtract_one_layer([nn_output, nn_at_one]) From 08d26f2221ca63e619c9066255bbdb4842ab90b9 Mon Sep 17 00:00:00 2001 From: Aron Date: Tue, 23 May 2023 16:28:54 +0200 Subject: [PATCH 34/68] Add option to save model plots (to current directory for now) --- n3fit/src/n3fit/model_trainer.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/n3fit/src/n3fit/model_trainer.py b/n3fit/src/n3fit/model_trainer.py index a9d3328966..ed8c8448f8 100644 --- a/n3fit/src/n3fit/model_trainer.py +++ b/n3fit/src/n3fit/model_trainer.py @@ -24,6 +24,8 @@ from n3fit.vpinterface import N3PDF from validphys.photon.compute import Photon +from tensorflow.keras.utils import plot_model + log = logging.getLogger(__name__) # Threshold defaults @@ -175,6 +177,7 @@ def __init__( self.max_cores = max_cores self.model_file = model_file self.print_summary = True + self.plot_model = True self.mode_hyperopt = False self.impose_sumrule = sum_rules self._hyperkeys = None @@ -252,9 +255,11 @@ def set_hyperopt(self, hyperopt_on, keys=None, status_ok="ok"): self._hyperkeys = keys if hyperopt_on: self.print_summary = False + self.plot_model = False self.mode_hyperopt = True else: self.print_summary = True + self.plot_model = True self.mode_hyperopt = False ########################################################################### @@ -472,6 +477,21 @@ def _model_generation(self, xinput, pdf_models, partition, partition_idx): if self.print_summary: training.summary() + pdf_model = training.get_layer("PDF_0") + pdf_model.summary() + nn_model = pdf_model.get_layer("NN_0") + nn_model.summary() + msr_model = pdf_model.get_layer("impose_msr") + msr_model.summary() + if self.plot_model: + log.info("Generating model plots saved in the current directory") + plot_model(training, to_file="full_model.png", show_shapes=True) + pdf_model = training.get_layer("PDF_0") + plot_model(pdf_model, to_file="pdf_model.png", show_shapes=True) + nn_model = pdf_model.get_layer("NN_0") + plot_model(nn_model, to_file="nn_model.png", show_shapes=True) + msr_model = pdf_model.get_layer("impose_msr") + plot_model(msr_model, to_file="msr_model.png", show_shapes=True) models = { "training": training, From 577d5d8bf50b60c3d23e95cacb7aca59900722d6 Mon Sep 17 00:00:00 2001 From: Aron Date: Thu, 25 May 2023 13:50:52 +0200 Subject: [PATCH 35/68] Add test for xDivide --- n3fit/src/n3fit/tests/test_xops.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 n3fit/src/n3fit/tests/test_xops.py diff --git a/n3fit/src/n3fit/tests/test_xops.py b/n3fit/src/n3fit/tests/test_xops.py new file mode 100644 index 0000000000..8aba9595ee --- /dev/null +++ b/n3fit/src/n3fit/tests/test_xops.py @@ -0,0 +1,33 @@ +""" + Test the x operations +""" +import numpy as np +from n3fit.layers import xDivide + + +def test_xdivide_default(): + """Check that the default xDivide works as expected""" + x_div = xDivide() + test_input = np.array([1, 2, 3], dtype=np.float32).reshape((1, 3, 1)) + test_output = x_div(test_input) + + expected_output = np.ones(shape=(1, 3, 14)) + default_indices = [3, 4, 5, 6] + for i in default_indices: + expected_output[:, :, i] = 1 / test_input[:, :, 0] + + assert np.allclose(test_output, expected_output) + +def test_xdivide_indices(): + """Check that the default xDivide works as expected""" + custom_indices = [0, 1, 7] + x_div = xDivide(div_list=custom_indices) + test_input = np.array([1, 2, 3], dtype=np.float32).reshape((1, 3, 1)) + test_output = x_div(test_input) + + expected_output = np.ones(shape=(1, 3, 14)) + for i in custom_indices: + expected_output[:, :, i] = 1 / test_input[:, :, 0] + + assert np.allclose(test_output, expected_output) + From f798c224a8f3eff53336f00d0e14380ff1423843 Mon Sep 17 00:00:00 2001 From: Aron Date: Thu, 25 May 2023 12:37:37 +0200 Subject: [PATCH 36/68] Update xDivide documentation to include v15 in the default settings, as the code already did. --- n3fit/src/n3fit/layers/x_operations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/n3fit/src/n3fit/layers/x_operations.py b/n3fit/src/n3fit/layers/x_operations.py index f3cc9c32e9..2b9a0a86a0 100644 --- a/n3fit/src/n3fit/layers/x_operations.py +++ b/n3fit/src/n3fit/layers/x_operations.py @@ -19,8 +19,8 @@ class xDivide(MetaLayer): """ Divide some PDFs by x - By default it utilizes the 14-flavour FK basis and divides [v, v3, v8] - which corresponds to indices (3,4,5) from + By default it utilizes the 14-flavour FK basis and divides [v, v3, v8, v15] + which corresponds to indices (3,4,5, 6) from (photon, sigma, g, v, v3, v8, v15, v24, v35, t3, t8, t15, t24, t35) Parameters: @@ -28,7 +28,7 @@ class xDivide(MetaLayer): output_dim: int dimension of the pdf div_list: list - list of indices to be divided by X (by default [3,4,5]; [v, v3, v8] + list of indices to be divided by X (by default [3,4,5, 6]; [v, v3, v8, v15] """ def __init__(self, output_dim=BASIS_SIZE, div_list=None, **kwargs): From 00a4974da901959c9615fe20f8eee0a335733f4f Mon Sep 17 00:00:00 2001 From: Aron Date: Thu, 25 May 2023 13:21:18 +0200 Subject: [PATCH 37/68] Simplify xDivide layer --- .../backends/keras_backend/operations.py | 18 ++++++++++++- n3fit/src/n3fit/layers/x_operations.py | 27 ++++++++++--------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index e348649481..b330f85193 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -203,7 +203,6 @@ def tensor_ones_like(*args, **kwargs): """ return K.ones_like(*args, **kwargs) - @tf.function def many_replication(grid, replications, axis=0, **kwargs): """ @@ -313,6 +312,14 @@ def tensor_product(*args, **kwargs): return tf.tensordot(*args, **kwargs) +@tf.function +def pow(tensor, power): + """ + Computes the power of the tensor + """ + return tf.pow(tensor, power) + + @tf.function(experimental_relax_shapes=True) def op_log(o_tensor, **kwargs): """ @@ -346,6 +353,15 @@ def scatter_to_one(values, indices=[[1]], output_dim=14): ones = np.ones(output_dim, dtype=np.float32) return tf.tensor_scatter_nd_update(ones, indices, values) +def scatter_to_zero(values, indices, output_dim): + """ + Like scatter_nd initialized to zero + see full `docs `_ + """ + indices = tf.constant([[i] for i in indices]) + updates = tf.constant(values) + return tf.scatter_nd(indices, updates, [output_dim]) + def op_subtract(inputs, **kwargs): """ diff --git a/n3fit/src/n3fit/layers/x_operations.py b/n3fit/src/n3fit/layers/x_operations.py index 2b9a0a86a0..b1593980c4 100644 --- a/n3fit/src/n3fit/layers/x_operations.py +++ b/n3fit/src/n3fit/layers/x_operations.py @@ -1,7 +1,7 @@ """ This module contains layers acting on the x-grid input of the NN - The three operations included are: + The two operations included are: - ``xDivide`` - ``xIntegrator`` @@ -9,6 +9,7 @@ for all flavours. The choice of flavours on which to act in a different way is given as an input argument. """ +from typing import List from n3fit.backends import MetaLayer from n3fit.backends import operations as op @@ -31,24 +32,26 @@ class xDivide(MetaLayer): list of indices to be divided by X (by default [3,4,5, 6]; [v, v3, v8, v15] """ - def __init__(self, output_dim=BASIS_SIZE, div_list=None, **kwargs): + def __init__(self, output_dim: int = BASIS_SIZE, div_list: List = None, **kwargs): if div_list is None: div_list = [3, 4, 5, 6] self.output_dim = output_dim self.div_list = div_list super().__init__(**kwargs) + # Create powers, a vector of zeros except for the indices + self.powers = op.scatter_to_zero( + indices=div_list, + values=[-1.0] * len(div_list), + output_dim=output_dim) + def call(self, x): - out_array = [] - one = op.tensor_ones_like(x) - for i in range(self.output_dim): - if i in self.div_list: - res = one / x - else: - res = one - out_array.append(res) - out_tensor = op.concatenate(out_array) - return out_tensor + return op.pow(x, self.powers) + + def get_config(self): + config = super().get_config() + config.update({"output_dim": self.output_dim, "div_list": self.div_list}) + return config class xIntegrator(MetaLayer): From 4c94100f4f2201d6e375197772f088515f615be1 Mon Sep 17 00:00:00 2001 From: Aron Date: Thu, 25 May 2023 13:32:51 +0200 Subject: [PATCH 38/68] Clarify xDivide documentation. --- n3fit/src/n3fit/layers/x_operations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/n3fit/src/n3fit/layers/x_operations.py b/n3fit/src/n3fit/layers/x_operations.py index b1593980c4..76e1d41567 100644 --- a/n3fit/src/n3fit/layers/x_operations.py +++ b/n3fit/src/n3fit/layers/x_operations.py @@ -18,7 +18,8 @@ class xDivide(MetaLayer): """ - Divide some PDFs by x + Create tensor of either 1/x or ones depending on the flavour, + to be used to divide some PDFs by x by multiplying with the result. By default it utilizes the 14-flavour FK basis and divides [v, v3, v8, v15] which corresponds to indices (3,4,5, 6) from From 4f3d949bf2d86de801ae3463458d1e65eea65bb9 Mon Sep 17 00:00:00 2001 From: Aron Date: Thu, 25 May 2023 16:58:28 +0200 Subject: [PATCH 39/68] Turn off plotting of model for now --- n3fit/src/n3fit/model_trainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/n3fit/src/n3fit/model_trainer.py b/n3fit/src/n3fit/model_trainer.py index ed8c8448f8..eab6936095 100644 --- a/n3fit/src/n3fit/model_trainer.py +++ b/n3fit/src/n3fit/model_trainer.py @@ -177,7 +177,7 @@ def __init__( self.max_cores = max_cores self.model_file = model_file self.print_summary = True - self.plot_model = True + self.plot_model = False # TODO: change back to true once dependencies fixed self.mode_hyperopt = False self.impose_sumrule = sum_rules self._hyperkeys = None @@ -259,7 +259,7 @@ def set_hyperopt(self, hyperopt_on, keys=None, status_ok="ok"): self.mode_hyperopt = True else: self.print_summary = True - self.plot_model = True + self.plot_model = False # TODO: change back to true once dependencies fixed self.mode_hyperopt = False ########################################################################### From b2790681607749132033a3497cb6e9fe75c64727 Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 31 May 2023 14:16:51 +0200 Subject: [PATCH 40/68] Manually change regression weights hdf5 file structure to fit new model --- .../src/n3fit/tests/regressions/weights_1.h5 | Bin 33624 -> 29064 bytes .../src/n3fit/tests/regressions/weights_2.h5 | Bin 33624 -> 29064 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/n3fit/src/n3fit/tests/regressions/weights_1.h5 b/n3fit/src/n3fit/tests/regressions/weights_1.h5 index bcc79666b09430dbd2d850aa7388427ff4e8108b..2c5b02e71eed2ab94b30aa51f0cfb9264fc4ba55 100644 GIT binary patch literal 29064 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 Q7d(PgJY4Y5)d@WQ4;SlD;{X5v literal 33624 zcmeHQ3s_Xu7Cr+$CI!NLp{Y#>qG>)+QReI!3vE&}Q!^|Kh8P9qdM*9*%H>wZJ!ke>FmqtQ4`skS=lf*8*4}&V z|E#t4-s|kyE;fXX@NUtu1t&dfHP?jmwMX$!IXvMDSq`SLJPINz2s0tf@n8`Z*Nl_O z{2<>K+ShopjC`m#VchWH9Oubl`xrvCiUu^0(S00?7$9vivPd4JmEg~XfMNKsvC@d5 zKwb)Ac$z2{QHhdUot$M(WaSnXh&0vHWlwUZ19Gkv$g5ngy|`;)OPtuA7pE4bJKi|U zWR6TUCYVy}Ra=Y9cuD0d#_)sCAck>?$!V#P{p`6c-rpXNHzq{K7#VSlDKW(a_3Uw= zXK_ZK95W>{*=$NSCvjXxWK3LoT#UU+LRx%kTyp#@sXE4`>J@RZdPZbiVybDX*_fJS zHl-)Ur=`XvC5rTkN#+D&eB5l4nG?q)@dF);WnP>f#;xYsh$D|SzB1jE7{hfChbdVO z=^q=hT8v?wHI814d~xGCrbI9Jt|RxrM7VX zM0-@yk+rJ^xpXDxc|92&%DJ4Ir=EOvw4GNN5c16y1RR$t+Mp!!l=!5XtR_tBoL&FK ztp2@&d-ru{G!G4&L!J^<5Ks_M5Ks_M5Ks_M5Ks_M5Ks_M5Ks_M5Ks_kSOmt8c{WV) z9iU&USaNEL5BdSru+i@heh&0|6ha#5G(A{7J@^~YUy(O}36tuaoEsm>aj{P2^_{5AzgyUaae#VI z-~1@;H!cL^{#^~Q*B$(;$SrCjrcoCqxt5*~7Toq8nSI6@%nd98n^NqU8UN`N37e-f7)_B)vX_9`!1vf3F_37j&B`GdPT;Nb2%T; zQLp`v-#smhZuspnlDk!F`=w|VY0>dja%!}JE-VPtmCf9s4LiwM=RbdZ-+|esp{61J zR`ZUo+6iZav?a5D8hSPG<$alP{#KvFZra}i1FhRaI#}Py4bZ0A0=4Of0=46YY~s_h z2=yzl(ut8X$h;m0guI@+gef;}@Ux8C&77xcQwzx8ndpLnwwE$QN|J$N9S&Kc#3&?`qt;FyoejU%_{S>r~b(^nJ7`S1IXF5i}r{~UUqzCL*!*)wy}zCEjM(d$ny z6|P>e3JF(d(9Rb&^XiC6#4tc0ub#~nM&|!v^I-;ULK@i%2*n^Jxmy9Tgxw1?Ivqq>u)VPs?u@1 zFQ4075IkEwWYhfkIUoFeDZk=GIo+@Rgq*!KhNNvT5(;8VtjpVMA*zc_YzumM@B>e& zZGqpE(B%nx_;)9i3!a*#^r7fhz zX#TUQ)(J)BLfbAs^0q7Q()_0HQ}4d5Nb(bzBy4&oa_GO@KqDEnp^4Vk=4=y2|9df3~C zcwFvA;wF~ylbRhCQX(!|Jw|lXjtB_Q{{4X9tQ|y|``)0^PB|K2Ej>Rs zbmd1u+BdH2bx*&xk;G?hAe*DD^n+6Yx~8o+(eUxx`L>%+5cB%MLg}VbVfF6=>9T-g zVRVm4#IH>-2|XWdT|hSTBZ9Wj$by@~q+Y+!0|N@l`E%3w_d|n7c|tp3-mzA;kcgG! z!LdQM-#Yde-fJ64GG9rvcFqsB>HIGUSsw3dO+JHY{`I%XzkX^>fBm79OpZIOz0u`8 zGUN|Wnm_Uw-En>$`C^ej{q(eX?_jcsp0Zvf+K1QB(YeQIYVW~1qldR`Uwi}|_?5qI z$kxl&7w2gB{ZsV1i0_usTgyFltDjVnce?E2I|aA29h^IyAN1wVWci@S`EAeLv=;5i zri*4=Bd5cf=vw!CiQWj`E<}wQ#J4}5L7K&O)wOF;Ok<9el2u{XLQnqFS9h}KC~_dT z4V|CF)0D$)Z9ZMM2_LTRruBHEt2MwJsBNvy-utNOQSI)8KFo#JA9GE9dlo`(!oQIwnYecxO9ZzgV^Y$GKZ|yGEYZTW)-*U_+-_4k?HU&OHP`@~EQV^ZtHYt? z+p;{)I?wLI!rJUh6gwhX;U91V>q-}m2m!f$(ExAxAV0`zefy=tL2l8{C6{C5LkXP` zko%=_*s*PAB8L>NBMiM9GL@(}Z?fN!-~_4yPan=>IImz>_xYq#R|YqmzYzMW^GU<5 zOI@A!&4BM}S-^O0^UIl`j1c7x$E$?*2Lkf>1=p_{kRwEatqc33`rWnltNeTL-C4i( zm#ts>-|_kt?F#CNJY9Et69)E1k5j81@~ZYG7GH&1LRafoJ$&Gb0=rS0ebK=79&vLE z^GYX;1_8N!(Ew)_kRN2UzSpmQAh)Pwaydpml+YOg`T8}McP^KwP|wwa@NHpoG;S;7Wg@K#s6Z zed&*8IO{{zMsA4;JXJq4g(Z4DbU3 zSM&!ZKnbfuz?J?KKjTbYU;2|hiuIEZ{WY}yB#&qOSYY^y{s0h4P%Q$k^hfiYGx^=t zA6MU3a!jn>Z0H~PivkAJ5EiE~I#+&xeE6ZB#d#QYPBA}l6Rt0`kT;A)5EuDA4&3cr z8SLwjPQG5%Lwg}G45VM8L|N-qp>Z!GdwUwWZ_f%VWG`g50F zxLRKnM=*XYFmPpF0U(s1S_E9_PbSC_R;n-kF}%onh=u+dT7L?o89zlZV5UE=?;kFl z%II-EM!b~p{y;#kKYlRmLh&XQ$H(zLa48@4N$*Hhhq(Rxr&tev$AsrW9q$d7n#Fs~ z@lJER&s=_wINn2!tY8{%8m~yhd3nb>$)!5-aXQ|8E|ojpZ7!t|hc!~75j|{Q-CVpS zh&_3*FGPOj`@~qFD}-M=A)S2PoB?##WOVX%L?+N_z&>Joa(jumMNMWEkS-I_IUt8Q z&|W_5YmqK{4lBq>VqqT8X_8q4>GT$sj#>Z&w;ABRTF#Fbw?@@OY(vGlzJIBmR1Wv_ z<7o>FE9I<@F+Mrg7@3kdb!cCQ+-OrOWID(s(2A6flG?{kYtO7on_eBQJ+m6x0V1)S zhjb~^F_-b19#S2xlv$NFwYqlgnbpu%*KTU{?Y@#+4G;EA5qU(~`9;@uUJm62!@8fB z+Ze!lmZYTeX>8}^*!sZ3rbU9G0hyQQE6?GvLM9p$V!SZ9FI6S## zx=bj}5dY7YnjQlbW+v1`CC#@r|6UL6HimV#-#Pb${dON7TNFG*(X70W@)HFQCp>m4 zcsSvacTeCU-~X$@{1(D~3X&E3+_9K(hoLL~?-|&iSzNU5K6_`qC*Gg?@W@y25M{E` z{(p8);DPJTGSH{2zq(K?$PR%pBfe%jKPV2LRx@256pziQneGe}C(o{#4x5RcQ8V2d zC@xB^nJyfP*Cf_V=RQ3vyeH%e(Q*|MH0*H{-Mh9iX@aXUsFYSWfIFPD%kmB?k0&G%F(9;{8uI`=0%hvd!@VZ$9?j4 z^q!D6_u)~b;33Ly? nEsA`)d^Jjeh=41vmtOoI{L$9? diff --git a/n3fit/src/n3fit/tests/regressions/weights_2.h5 b/n3fit/src/n3fit/tests/regressions/weights_2.h5 index cd7b3d2d3c8609e5235e285c0c8a72c8087226e7..e5a8adea8692c6ef3fab1d8947de89455ce607ca 100644 GIT binary patch literal 29064 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 Date: Wed, 31 May 2023 15:49:02 +0200 Subject: [PATCH 41/68] Manually change developing_weights.h5 file structure to fit new model --- n3fit/runcards/examples/developing_weights.h5 | Bin 53532 -> 41420 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/n3fit/runcards/examples/developing_weights.h5 b/n3fit/runcards/examples/developing_weights.h5 index a337c5b6a4430e87f23002a0518bf7b0b7636bf0..542fca06f960ec56e2db2a210f06c3c058c1bec0 100644 GIT binary patch delta 4582 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 Date: Thu, 1 Jun 2023 10:24:45 +0200 Subject: [PATCH 42/68] Remove inp option in pdfNN_layer_generator, instead enforcing adding of logs if scaler is None. --- n3fit/src/n3fit/model_gen.py | 54 +++++++++++++++--------------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index c24fc17f8c..190f352e35 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -399,7 +399,6 @@ def output_layer(ilayer): def pdfNN_layer_generator( - inp: int = 2, nodes: List[int] = None, activations: List[str] = None, initializer_name: str = "glorot_normal", @@ -479,8 +478,6 @@ def pdfNN_layer_generator( Parameters ---------- - inp: int - dimension of the xgrid. If inp=2, turns the x point into a (x, log(x)) pair nodes: list(int) list of the number of nodes per layer of the PDF NN. Default: [15,8] activation: list @@ -502,9 +499,10 @@ def pdfNN_layer_generator( rate of dropout layer by layer impose_sumrule: str whether to impose sumrules on the output pdf and which one to impose (All, MSR, VSR) - scaler: scaler + scaler: callable Function to apply to the input. If given the input to the model will be a (1, None, 2) tensor where dim [:,:,0] is scaled + When None, instead turn the x point into a (x, log(x)) pair parallel_models: int How many models should be trained in parallel photon: :py:class:`validphys.photon.compute.Photon` @@ -544,41 +542,35 @@ def pdfNN_layer_generator( # The number of nodes in the last layer is equal to the number of fitted flavours last_layer_nodes = nodes[-1] # (== len(flav_info)) - # Process input options. There are 3 options: - # 1. Do nothing - # 2. Scale the input - # 3. Concatenate log(x) to the input - # when feature scaling is on, don't add logs regardless of the input + # Process input options. There are 2 options: + # 1. Scale the input + # 2. Concatenate log(x) to the input use_feature_scaling = scaler is not None - add_logs = inp == 2 and not use_feature_scaling + add_logs = not use_feature_scaling # When scaler is active we also want to do the subtraction of large x # TODO: make it its own option (i.e., one could want to use this without using scaler) subtract_one = use_feature_scaling - if use_feature_scaling: - inp = 1 + # Feature scaling happens before the pdf model and changes x->(scaler(x), x), + # so it adds an input dimension + pdf_input_dimensions = 2 if use_feature_scaling else 1 + # Adding of logs happens inside, but before the NN and adds a dimension there + nn_input_dimensions = 1 if use_feature_scaling else 2 # Define the main input do_nothing = lambda x: x - if add_logs: - placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') - process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='x_logx') - extract_original = do_nothing - extract_nn_input = do_nothing - elif use_feature_scaling: - # Note feature scaling happens before the model created here, - # so the input is of the form (scaler(x), x) - placeholder_input = Input(shape=(None, 2), batch_size=1, name='scaledx_x') + if use_feature_scaling: + pdf_input = Input(shape=(None, pdf_input_dimensions), batch_size=1, name='scaledx_x') process_input = do_nothing - extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name='x_original') extract_nn_input = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') - else: - placeholder_input = Input(shape=(None, 1), batch_size=1, name='x') - process_input = do_nothing + extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, 1, axis=-1), name='x') + else: # if add_logs + pdf_input = Input(shape=(None, pdf_input_dimensions), batch_size=1, name='x') + process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='x_logx') extract_original = do_nothing extract_nn_input = do_nothing - model_input = {"pdf_input": placeholder_input} + model_input = {"pdf_input": pdf_input} if subtract_one: input_x_eq_1 = [1.0] @@ -630,7 +622,7 @@ def pdfNN_layer_generator( nn_replicas.append( generate_nn( layer_type=layer_type, - inp=inp, + input_dimensions=nn_input_dimensions, nodes=nodes, activations=activations, initializer_name=initializer_name, @@ -674,7 +666,7 @@ def compute_unnormalized_pdf(x, neural_network, compute_prefactor): # Finally compute the normalized PDFs for each replica pdf_models = [] for i_replica, (prefactor, nn) in enumerate(zip(prefactor_replicas, nn_replicas)): - pdf_unnormalized = compute_unnormalized_pdf(placeholder_input, nn, prefactor) + pdf_unnormalized = compute_unnormalized_pdf(pdf_input, nn, prefactor) pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, prefactor) pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input]) @@ -693,7 +685,7 @@ def compute_unnormalized_pdf(x, neural_network, compute_prefactor): def generate_nn( layer_type: str, - inp: int, + input_dimensions: int, nodes: List[int], activations: List[str], initializer_name: str, @@ -707,7 +699,7 @@ def generate_nn( Create the part of the model that contains all of the actual neural network layers. """ - common_args = {'nodes_in': inp, 'nodes': nodes, 'activations': activations, 'initializer_name': initializer_name, 'seed': replica_seed} + common_args = {'nodes_in': input_dimensions, 'nodes': nodes, 'activations': activations, 'initializer_name': initializer_name, 'seed': replica_seed} if layer_type == "dense": reg = regularizer_selector(regularizer, **regularizer_args) list_of_pdf_layers = generate_dense_network(**common_args, dropout_rate=dropout, regularizer=reg) @@ -716,7 +708,7 @@ def generate_nn( # Note: using a Sequential model would be more appropriate, but it would require # creating a MetaSequential model. - x = Input(shape=(None, inp), batch_size=1, name='xgrids_processed') + x = Input(shape=(None, input_dimensions), batch_size=1, name='xgrids_processed') pdf = x for layer in list_of_pdf_layers: pdf = layer(pdf) From 38a1ccbcf1cbfa41540e6ceef13c9d96acdc117f Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 2 Jun 2023 12:54:08 +0200 Subject: [PATCH 43/68] Incorporate Juan's comments --- .../backends/keras_backend/operations.py | 7 +++++- n3fit/src/n3fit/layers/preprocessing.py | 12 +++++----- n3fit/src/n3fit/model_gen.py | 24 +++++++++---------- n3fit/src/n3fit/tests/test_xops.py | 4 ++-- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index b330f85193..3abba9cd90 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -112,7 +112,12 @@ def batchit(x, batch_dimension=0, **kwarg): # layer generation -def numpy_to_input(numpy_array, no_reshape=False, name=None, custom_shape: tuple = None): +def numpy_to_input( + numpy_array: np.ndarray, + no_reshape: bool = False, + name: str = None, + custom_shape: tuple = None, + ): """ Takes a numpy array and generates a Input layer. By default it adds a batch dimension (of size 1) so that the shape of the layer diff --git a/n3fit/src/n3fit/layers/preprocessing.py b/n3fit/src/n3fit/layers/preprocessing.py index 9ad1ca3b61..d3e3b88a32 100644 --- a/n3fit/src/n3fit/layers/preprocessing.py +++ b/n3fit/src/n3fit/layers/preprocessing.py @@ -5,11 +5,11 @@ class Preprocessing(MetaLayer): """ - Computes prefactor for the PDF. + Computes preprocessing factor for the PDF. This layer generates a factor (1-x)^beta*x^(1-alpha) where both beta and alpha - are model paramters that can be trained. If feature scaling is used, the - prefactor is x^(1-alpha). + are model paramters that can be trained. If feature scaling is used, the preprocessing + factor is x^(1-alpha). Alpha is initialized uniformly within the ranges allowed in the runcard and then it is only allowed to move between those two values (with a hard wall in each side) @@ -21,7 +21,7 @@ class Preprocessing(MetaLayer): Parameters ---------- flav_info: list - list of dicts containing the information about the fitting of the prefactor + list of dicts containing the information about the fitting of the preprocessing factor This corresponds to the `fitting::basis` parameter in the nnpdf runcard. The dicts can contain the following fields: `smallx`: range of alpha @@ -29,7 +29,7 @@ class Preprocessing(MetaLayer): `trainable`: whether these alpha-beta should be trained during the fit (defaults to true) large_x: bool - Whether large x prefactor should be active + Whether large x preprocessing factor should be active seed: int seed for the initializer of the random alpha and beta values """ @@ -43,7 +43,7 @@ def __init__( **kwargs, ): if flav_info is None: - raise ValueError("Trying to instantiate a prefactor with no basis information") + raise ValueError("Trying to instantiate a preprocessing factor with no basis information") self.flav_info = flav_info self.seed = seed self.output_dim = len(flav_info) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 190f352e35..fe5bac218a 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -9,7 +9,7 @@ """ -from typing import List +from typing import List, Callable from dataclasses import dataclass import numpy as np @@ -302,13 +302,13 @@ def observable_generator( # Network generation functions def generate_dense_network( - nodes_in, - nodes, - activations, - initializer_name="glorot_normal", - seed=0, - dropout_rate=0.0, - regularizer=None, + nodes_in: int, + nodes: int, + activations: List[str], + initializer_name: str = "glorot_normal", + seed: int = 0, + dropout_rate: float = 0.0, + regularizer: str = None, ): """ Generates a dense network @@ -404,14 +404,14 @@ def pdfNN_layer_generator( initializer_name: str = "glorot_normal", layer_type: str = "dense", flav_info: dict = None, - fitbasis="NN31IC", + fitbasis: str = "NN31IC", out: int = 14, seed: int = None, dropout: float = 0.0, - regularizer=None, - regularizer_args=None, + regularizer: str = None, + regularizer_args: dict = None, impose_sumrule: str = None, - scaler=None, + scaler: Callable = None, parallel_models: int = 1, photons=None, ): # pylint: disable=too-many-locals diff --git a/n3fit/src/n3fit/tests/test_xops.py b/n3fit/src/n3fit/tests/test_xops.py index 8aba9595ee..cfdd95fc46 100644 --- a/n3fit/src/n3fit/tests/test_xops.py +++ b/n3fit/src/n3fit/tests/test_xops.py @@ -16,7 +16,7 @@ def test_xdivide_default(): for i in default_indices: expected_output[:, :, i] = 1 / test_input[:, :, 0] - assert np.allclose(test_output, expected_output) + np.testing.assert_allclose(test_output, expected_output, rtol=1e-05) def test_xdivide_indices(): """Check that the default xDivide works as expected""" @@ -29,5 +29,5 @@ def test_xdivide_indices(): for i in custom_indices: expected_output[:, :, i] = 1 / test_input[:, :, 0] - assert np.allclose(test_output, expected_output) + np.testing.assert_allclose(test_output, expected_output, rtol=1e-05) From 789a4c32bc052492e2109840f1db6e2b9e91ca85 Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 2 Jun 2023 17:36:07 +0200 Subject: [PATCH 44/68] Incorporate Roy's comments --- n3fit/src/n3fit/layers/x_operations.py | 10 ++----- n3fit/src/n3fit/model_gen.py | 38 ++++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/n3fit/src/n3fit/layers/x_operations.py b/n3fit/src/n3fit/layers/x_operations.py index 76e1d41567..c56a369a75 100644 --- a/n3fit/src/n3fit/layers/x_operations.py +++ b/n3fit/src/n3fit/layers/x_operations.py @@ -22,7 +22,7 @@ class xDivide(MetaLayer): to be used to divide some PDFs by x by multiplying with the result. By default it utilizes the 14-flavour FK basis and divides [v, v3, v8, v15] - which corresponds to indices (3,4,5, 6) from + which corresponds to indices (3, 4, 5, 6) from (photon, sigma, g, v, v3, v8, v15, v24, v35, t3, t8, t15, t24, t35) Parameters: @@ -30,7 +30,7 @@ class xDivide(MetaLayer): output_dim: int dimension of the pdf div_list: list - list of indices to be divided by X (by default [3,4,5, 6]; [v, v3, v8, v15] + list of indices to be divided by X (by default [3, 4, 5, 6]; [v, v3, v8, v15] """ def __init__(self, output_dim: int = BASIS_SIZE, div_list: List = None, **kwargs): @@ -40,11 +40,7 @@ def __init__(self, output_dim: int = BASIS_SIZE, div_list: List = None, **kwargs self.div_list = div_list super().__init__(**kwargs) - # Create powers, a vector of zeros except for the indices - self.powers = op.scatter_to_zero( - indices=div_list, - values=[-1.0] * len(div_list), - output_dim=output_dim) + self.powers = [-1 if i in div_list else 0 for i in range(output_dim)] def call(self, x): return op.pow(x, self.powers) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index fe5bac218a..13c469eadb 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -32,6 +32,8 @@ ) from n3fit.layers.observable import is_unique +from validphys.photon.compute import Photon # only used for type hint here + @dataclass class ObservableWrapper: @@ -413,14 +415,14 @@ def pdfNN_layer_generator( impose_sumrule: str = None, scaler: Callable = None, parallel_models: int = 1, - photons=None, + photons: Photon = None, ): # pylint: disable=too-many-locals """ Generates the PDF model which takes as input a point in x (from 0 to 1) and outputs a basis of 14 PDFs. It generates the preprocessing of the x into a set (x, log(x)), the arbitrary NN to fit the form of the PDF - and the prefactor. + and the preprocessing factor. The funtional form of the output of this function is of: @@ -455,7 +457,7 @@ def pdfNN_layer_generator( A function is constructed that joins all those layers. The function takes a tensor as the input and applies all layers for NN in order. - 4. Create a prefactor layer (that takes as input the same tensor x as the NN) + 4. Create a preprocessing factor layer (that takes as input the same tensor x as the NN) and multiply it to the NN. We have now: N(x)_{j} * x^{1-alpha_{j}} * (1-x)^{beta_{j}} @@ -546,7 +548,7 @@ def pdfNN_layer_generator( # 1. Scale the input # 2. Concatenate log(x) to the input use_feature_scaling = scaler is not None - add_logs = not use_feature_scaling + # When scaler is active we also want to do the subtraction of large x # TODO: make it its own option (i.e., one could want to use this without using scaler) subtract_one = use_feature_scaling @@ -564,7 +566,7 @@ def pdfNN_layer_generator( process_input = do_nothing extract_nn_input = Lambda(lambda x: op.op_gather_keep_dims(x, 0, axis=-1), name='x_scaled') extract_original = Lambda(lambda x: op.op_gather_keep_dims(x, 1, axis=-1), name='x') - else: # if add_logs + else: # add log(x) pdf_input = Input(shape=(None, pdf_input_dimensions), batch_size=1, name='x') process_input = Lambda(lambda x: op.concatenate([x, op.op_log(x)], axis=-1), name='x_logx') extract_original = do_nothing @@ -584,8 +586,8 @@ def pdfNN_layer_generator( custom_shape=(None, 1)) # Just to make shapes consistent model_input["layer_x_eq_1"] = layer_x_eq_1 - # the layer that multiplies the NN output by the prefactor - apply_prefactor = Lambda(op.op_multiply, name='prefactor_times_NN') + # the layer that multiplies the NN output by the preprocessing factor + apply_preprocessing_factor = Lambda(op.op_multiply, name='prefactor_times_NN') # Photon layer layer_photon = AddPhoton(photons=photons, name="add_photon") @@ -608,9 +610,9 @@ def pdfNN_layer_generator( # Only these layers change from replica to replica: nn_replicas = [] - prefactor_replicas = [] + preprocessing_factor_replicas = [] for i_replica, replica_seed in enumerate(seed): - prefactor_replicas.append( + preprocessing_factor_replicas.append( Preprocessing( flav_info=flav_info, input_shape=(1,), @@ -637,7 +639,7 @@ def pdfNN_layer_generator( # All layers have been made, now we need to connect them, # do this in a function so we can call it for both grids and each replica # Since all layers are already made, they will be reused - def compute_unnormalized_pdf(x, neural_network, compute_prefactor): + def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): # Preprocess the input grid x_nn_input = extract_nn_input(x) x_original = extract_original(x) @@ -650,9 +652,9 @@ def compute_unnormalized_pdf(x, neural_network, compute_prefactor): nn_at_one = neural_network(x_eq_1_processed) nn_output = subtract_one_layer([nn_output, nn_at_one]) - # Compute the preprocessing prefactor and multiply - prefactor = compute_prefactor(x_original) - pref_nn = apply_prefactor([nn_output, prefactor]) + # Compute the preprocessing factor and multiply + preprocessing_factor = compute_preprocessing_factor(x_original) + pref_nn = apply_preprocessing_factor([nn_output, preprocessing_factor]) # Apply basis rotation if needed if not basis_rotation.is_identity(): @@ -665,12 +667,14 @@ def compute_unnormalized_pdf(x, neural_network, compute_prefactor): # Finally compute the normalized PDFs for each replica pdf_models = [] - for i_replica, (prefactor, nn) in enumerate(zip(prefactor_replicas, nn_replicas)): - pdf_unnormalized = compute_unnormalized_pdf(pdf_input, nn, prefactor) - pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, prefactor) + for i_replica, (preprocessing_factor, nn) in \ + enumerate(zip(preprocessing_factor_replicas, nn_replicas)): + pdf_unnormalized = compute_unnormalized_pdf( + pdf_input, nn, preprocessing_factor) + pdf_integration_grid = compute_unnormalized_pdf( + integrator_input, nn, preprocessing_factor) pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input]) - # Final PDF (apply normalization) if photons: pdf_normalized = layer_photon(pdf_normalized, i_replica) From fe84657e10a0e9b8128ac96bf99f836253fbcbf3 Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 5 Jun 2023 09:05:52 +0200 Subject: [PATCH 45/68] Remove plotting of model from model_trainer --- n3fit/src/n3fit/model_trainer.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/n3fit/src/n3fit/model_trainer.py b/n3fit/src/n3fit/model_trainer.py index eab6936095..974dd18b16 100644 --- a/n3fit/src/n3fit/model_trainer.py +++ b/n3fit/src/n3fit/model_trainer.py @@ -24,8 +24,6 @@ from n3fit.vpinterface import N3PDF from validphys.photon.compute import Photon -from tensorflow.keras.utils import plot_model - log = logging.getLogger(__name__) # Threshold defaults @@ -177,7 +175,6 @@ def __init__( self.max_cores = max_cores self.model_file = model_file self.print_summary = True - self.plot_model = False # TODO: change back to true once dependencies fixed self.mode_hyperopt = False self.impose_sumrule = sum_rules self._hyperkeys = None @@ -255,11 +252,9 @@ def set_hyperopt(self, hyperopt_on, keys=None, status_ok="ok"): self._hyperkeys = keys if hyperopt_on: self.print_summary = False - self.plot_model = False self.mode_hyperopt = True else: self.print_summary = True - self.plot_model = False # TODO: change back to true once dependencies fixed self.mode_hyperopt = False ########################################################################### @@ -483,15 +478,6 @@ def _model_generation(self, xinput, pdf_models, partition, partition_idx): nn_model.summary() msr_model = pdf_model.get_layer("impose_msr") msr_model.summary() - if self.plot_model: - log.info("Generating model plots saved in the current directory") - plot_model(training, to_file="full_model.png", show_shapes=True) - pdf_model = training.get_layer("PDF_0") - plot_model(pdf_model, to_file="pdf_model.png", show_shapes=True) - nn_model = pdf_model.get_layer("NN_0") - plot_model(nn_model, to_file="nn_model.png", show_shapes=True) - msr_model = pdf_model.get_layer("impose_msr") - plot_model(msr_model, to_file="msr_model.png", show_shapes=True) models = { "training": training, From 1f4537edb13c2acbb1e82d412f097bd4dcd59f9b Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 5 Jun 2023 09:29:26 +0200 Subject: [PATCH 46/68] Remove unnecessary get_original layer from msr if scaler is not used --- n3fit/src/n3fit/msr.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index 4de24c6012..14222f5195 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -69,7 +69,11 @@ def generate_msr_model_and_grid( xgrid_integration, name="integration_grid", custom_shape=(None, grid_shape)) # 1c Get the original grid - x_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ")(xgrid_integration) + if scaler: + get_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ") + else: + get_original = lambda x: x + x_original = get_original(xgrid_integration) # 2. Divide the grid by x depending on the flavour x_divided = xDivide()(x_original) From 6353db3862476dea21155095a92acd4586d6762d Mon Sep 17 00:00:00 2001 From: Aron Date: Mon, 5 Jun 2023 10:04:11 +0200 Subject: [PATCH 47/68] Add model plot example script to documentation. --- doc/sphinx/source/n3fit/figures/plot_pdf.png | Bin 0 -> 89400 bytes doc/sphinx/source/n3fit/methodology.rst | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 doc/sphinx/source/n3fit/figures/plot_pdf.png diff --git a/doc/sphinx/source/n3fit/figures/plot_pdf.png b/doc/sphinx/source/n3fit/figures/plot_pdf.png new file mode 100644 index 0000000000000000000000000000000000000000..59d086a1392ec1c903468f8f416eadc2ded5764e GIT binary patch literal 89400 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%0A_;c=GI-I@$_~kHcqe!R(ud zPz5FKZp)WMah;U<&YT}qAbxd_s#+e@FBHv`kYE!Qk-6dCp+;d0>|+w272ZriTSXcw za$882%``aP{R3$r6Za0s;dmldhm8(YD?isY-<7fx?G} zG#WD2-rnB(0P+v@D*Ub7G+aVA#96o5FvKHXbeNFn{RA-C)mO-|gnSPa0b3w`6U`1c z96$Q44rM*3SV`t$#`yYA`6NoU8IUY7Tga_^x&f~iYx%kPL9)TV9_(trCs*s^e89H_ zDaxnz%k;3zUOKC=>S%Q(eM|oIX3~>=(z~uIo$l8F{1DPEDzWnfTi<2h)2`~5B8{sz zhqnj{x>nsX&ooE&X1djz>EoGnfbZrB{CoOE@58k@Zq2DJ=*mwc@HMGKtp0@i+;I{n z%p%Zx4z7G_As!*0&`?2QG+^qwsuXnC4q>E>7d%v@4+6(2n_v+LeJBD(hj=0NnOcsT zPkeEhPR9xNw#)d;={OaWP2+@5C%B*Z`|JNdT+q1wVVpVoFKDJSXQSklTnELl6dA@a znN^JJ9^-=?oEcD5PiA0yIfp|7vx=*9-v#;I!Pv4(Ky@u4zaH;tI{>cG6hgv_(FQ!m zm^UqF3usiXA;r!K2MiQ=`s|Zf9Oxmdavkd45JF7vwwkc95~+37!Ib z$?Sad>&c8o?5Vy>+Gz!FsWHLNdzUuf?m7 zs77{>$B%hHV*N>Q+d&RwI`d)mCnOc}3TIABBsd%C_pZ%gTAiQ`e09&oAY~9amJiU8 z^g>Ax2;2p#mhSMf5 z2*M2KKcU9eP2H_9KI+$GUn36oWl%UV)y?hR zqobr<@ZnpAHI!mh+p!QAMhOnmGIKNa+gVvP;ss~V7GF;kH;8jFFWZ6?!g=Pv{b|}Y zKw-f~n(2%qv0+IYiY~Owi^#$Sb%H=6&0e|ndB@&=7}7dKJ;kp6;;d7r5|<(EKSnI5X6s&aJWX2 zlvV;N0j{k;%MH-J@QO6Jvrtr!Gy~X=Dx#dCE3vx%JR^VH-$lQu^x%roWOOH(3_~))yU*prX{$VKtFeTBy z5nO{1YBh)*k0rr~AR3);QBJIn78Gn?2xveTa;dQpJA(AB>V;L0p&G{gv>C$kl%hu? zOu#FV>F$)z4Is;m7f`>X#ZRuneoGA_I%^pJ_#la>{c`o3vJ<|oC|rUl`Dyah5>dG3 zaiX4)&_NFbV$vVOMyJz^B7lSBcT6p}nYJQ01$tweL5>%UQHlp@vMbA$52_MI<$YHM zzA$`mlhN7~qt2&^7dJ6P5j^4X#a z@W2~UI@<09(hu{zGHUP)vGZ@^5T^)AjU++QXkQgIM__mGWTroTK7xVKG}?viHe;B> z^&Uu&@QdKVF$B6#M^Rz89*4?V9R4vl*ics(wi<-=gp=j~&Wwve51u@k3SZ)55G6Af z^WX@vw#rhy097Elk07=-bhm7Pr5Ya$s6brbRd9NA>R}`}Y(wX1p8-n4Z&?p*)Wq2_Hn|WWXeB3`QM35Dzw%DP&6my6@0&!uerGykxz=YX53v@U`a}3%Y0F(29IRu2IZWzTx4}f#jY&z&?ulJ#c(}QZ&y69 zQZ)JC4-oMbWGz7>cb`Ga3jG0vQ4=~4?$)T=BV#>Mh{TzHpor;~V@Wu+XjY|t$8@7+ zcZ!*DGE?WQ%) z@;kXlQVIwODX@S`*?Z4vEG!CoFux~Uz4kFqE_4s6!|9fyA8}tFQjP6 z4Ce9kYQ)CHy~E|7e9!Y+-&V3yqT+*%6*C&{BMh*T#{z2SN42%tFI6v`mKpe?*%)=! zH~>~?DY$C=dXx7XW}zFSZ-47O$ZN+9w0reKo zq5&cBugWm7aGTLfpk+1e1`$%o1bBIQi4JUX=VVbf_(T3oZg$i;-*`I z%WHMtH58rdc11jMIy&xcRvR6A2=O=Ki-cMgcbbEu79p1doI2#T%8@}H02YLOlYzcI z8Ss!DF!6+vUk=P}#qZ0Yl7ariAADotM(O^Wh%AIoDhAsN9g(m-zI)r}zAa@5v(9d& zIW@orJCB-aw$9-LaIhF$z-II>L}bBVoc%J=(;Q`wH^f$a02iq(fG{GyPqDuG!;B3P zOezk8-KlkY9HZXg)+B!$> z#>uZ#D<7Pi;qmGBjImYUPxkEgc(d){@t*Z!8+wXQ9%^2{4>D_h$l=#S?1*VN7$Uby zrApCyQE6Qm|1r*^?~M%&BN;D_W$$)htt=_^y`_kyrizNwD8*f=3eU z(nb7Z!NxFq7^MsiyEUke78piqKZw+@#yNNQ_kvD$rm1~+SWzir$3hX2bi+ipys*>p zX*z*1@_YPzgkBoc| zJY|2TN#`xbBb%^ONbK-=pVnWCjNF?4T3R-_B{;0aR^HdOU@=_g^cqc;dvhInj~J%Z zwhy^hbq(pCpLoj Date: Mon, 5 Jun 2023 15:20:17 +0200 Subject: [PATCH 48/68] Apply black to all changed files. --- .../backends/keras_backend/operations.py | 27 +++--- n3fit/src/n3fit/layers/preprocessing.py | 4 +- n3fit/src/n3fit/model_gen.py | 89 +++++++++++-------- n3fit/src/n3fit/msr.py | 31 ++++--- n3fit/src/n3fit/scaler.py | 5 +- n3fit/src/n3fit/tests/test_xops.py | 2 +- 6 files changed, 89 insertions(+), 69 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 3abba9cd90..ff3d1a0248 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -36,7 +36,7 @@ def evaluate(tensor): - """ Evaluate input tensor using the backend """ + """Evaluate input tensor using the backend""" return K.eval(tensor) @@ -107,17 +107,17 @@ def numpy_to_tensor(ival, **kwargs): # f(x: tensor) -> y: tensor def batchit(x, batch_dimension=0, **kwarg): - """ Add a batch dimension to tensor x """ + """Add a batch dimension to tensor x""" return tf.expand_dims(x, batch_dimension, **kwarg) # layer generation def numpy_to_input( - numpy_array: np.ndarray, - no_reshape: bool = False, - name: str = None, - custom_shape: tuple = None, - ): + numpy_array: np.ndarray, + no_reshape: bool = False, + name: str = None, + custom_shape: tuple = None, +): """ Takes a numpy array and generates a Input layer. By default it adds a batch dimension (of size 1) so that the shape of the layer @@ -183,7 +183,7 @@ def op_gather_keep_dims(tensor, indices, axis=0, **kwargs): both eager and non-eager tensors """ if indices == -1: - indices = tensor.shape[axis]-1 + indices = tensor.shape[axis] - 1 def tmp(x): y = tf.gather(x, indices, axis=axis, **kwargs) @@ -198,6 +198,7 @@ def tmp(x): # f(x: tensor[s]) -> y: tensor # + # Generation operations # generate tensors of given shape/content @tf.function @@ -208,6 +209,7 @@ def tensor_ones_like(*args, **kwargs): """ return K.ones_like(*args, **kwargs) + @tf.function def many_replication(grid, replications, axis=0, **kwargs): """ @@ -224,7 +226,7 @@ def many_replication(grid, replications, axis=0, **kwargs): # modify properties of the tensor like the shape or elements it has @tf.function def flatten(x): - """ Flatten tensor x """ + """Flatten tensor x""" return tf.reshape(x, (-1,)) @@ -248,7 +250,7 @@ def transpose(tensor, **kwargs): def stack(tensor_list, axis=0, **kwargs): - """ Stack a list of tensors + """Stack a list of tensors see full `docs `_ """ return tf.stack(tensor_list, axis=axis, **kwargs) @@ -288,8 +290,8 @@ def pdf_masked_convolution(raw_pdf, basis_mask): pdf_x_pdf: tf.tensor rank3 (len(mask_true), xgrid, xgrid, replicas) """ - if raw_pdf.shape[-1] == 1: # only one replica! - pdf = tf.squeeze(raw_pdf, axis=(0,-1)) + if raw_pdf.shape[-1] == 1: # only one replica! + pdf = tf.squeeze(raw_pdf, axis=(0, -1)) luminosity = tensor_product(pdf, pdf, axes=0) lumi_tmp = K.permute_dimensions(luminosity, (3, 1, 2, 0)) pdf_x_pdf = batchit(boolean_mask(lumi_tmp, basis_mask), -1) @@ -358,6 +360,7 @@ def scatter_to_one(values, indices=[[1]], output_dim=14): ones = np.ones(output_dim, dtype=np.float32) return tf.tensor_scatter_nd_update(ones, indices, values) + def scatter_to_zero(values, indices, output_dim): """ Like scatter_nd initialized to zero diff --git a/n3fit/src/n3fit/layers/preprocessing.py b/n3fit/src/n3fit/layers/preprocessing.py index d3e3b88a32..3aefbadaec 100644 --- a/n3fit/src/n3fit/layers/preprocessing.py +++ b/n3fit/src/n3fit/layers/preprocessing.py @@ -43,7 +43,9 @@ def __init__( **kwargs, ): if flav_info is None: - raise ValueError("Trying to instantiate a preprocessing factor with no basis information") + raise ValueError( + "Trying to instantiate a preprocessing factor with no basis information" + ) self.flav_info = flav_info self.seed = seed self.output_dim = len(flav_info) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 13c469eadb..33d6ec4e58 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -360,7 +360,6 @@ def generate_dense_per_flavour_network( number_of_layers = len(nodes) current_seed = seed for i, (nodes_out, activation) in enumerate(zip(nodes, activations)): - initializers = [] for _ in range(basis_size): # select the initializer and move the seed @@ -581,9 +580,8 @@ def pdfNN_layer_generator( # the layer that subtracts 1 from the NN output subtract_one_layer = Lambda(op.op_subtract, name='subtract_one') layer_x_eq_1 = op.numpy_to_input( - np.array(input_x_eq_1).reshape(1, 1), - name='x_ones', - custom_shape=(None, 1)) # Just to make shapes consistent + np.array(input_x_eq_1).reshape(1, 1), name='x_ones', custom_shape=(None, 1) + ) # Just to make shapes consistent model_input["layer_x_eq_1"] = layer_x_eq_1 # the layer that multiplies the NN output by the preprocessing factor @@ -593,14 +591,18 @@ def pdfNN_layer_generator( layer_photon = AddPhoton(photons=photons, name="add_photon") # Basis rotation - basis_rotation = FlavourToEvolution(flav_info=flav_info, fitbasis=fitbasis, name="pdf_evolution_basis") + basis_rotation = FlavourToEvolution( + flav_info=flav_info, fitbasis=fitbasis, name="pdf_evolution_basis" + ) # Evolution layer layer_evln = FkRotation(input_shape=(last_layer_nodes,), output_dim=out, name="pdf_FK_basis") # Normalization and sum rules if impose_sumrule: - sumrule_layer, integrator_input = generate_msr_model_and_grid(mode=impose_sumrule, scaler=scaler, photons=photons) + sumrule_layer, integrator_input = generate_msr_model_and_grid( + mode=impose_sumrule, scaler=scaler, photons=photons + ) model_input["integrator_input"] = integrator_input else: sumrule_layer = lambda x: x @@ -623,18 +625,19 @@ def pdfNN_layer_generator( ) nn_replicas.append( generate_nn( - layer_type=layer_type, - input_dimensions=nn_input_dimensions, - nodes=nodes, - activations=activations, - initializer_name=initializer_name, - replica_seed=replica_seed, - dropout=dropout, - regularizer=regularizer, - regularizer_args=regularizer_args, - last_layer_nodes=last_layer_nodes, - name=f"NN_{i_replica}") + layer_type=layer_type, + input_dimensions=nn_input_dimensions, + nodes=nodes, + activations=activations, + initializer_name=initializer_name, + replica_seed=replica_seed, + dropout=dropout, + regularizer=regularizer, + regularizer_args=regularizer_args, + last_layer_nodes=last_layer_nodes, + name=f"NN_{i_replica}", ) + ) # All layers have been made, now we need to connect them, # do this in a function so we can call it for both grids and each replica @@ -667,12 +670,11 @@ def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): # Finally compute the normalized PDFs for each replica pdf_models = [] - for i_replica, (preprocessing_factor, nn) in \ - enumerate(zip(preprocessing_factor_replicas, nn_replicas)): - pdf_unnormalized = compute_unnormalized_pdf( - pdf_input, nn, preprocessing_factor) - pdf_integration_grid = compute_unnormalized_pdf( - integrator_input, nn, preprocessing_factor) + for i_replica, (preprocessing_factor, nn) in enumerate( + zip(preprocessing_factor_replicas, nn_replicas) + ): + pdf_unnormalized = compute_unnormalized_pdf(pdf_input, nn, preprocessing_factor) + pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, preprocessing_factor) pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input]) @@ -687,28 +689,40 @@ def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): return pdf_models + def generate_nn( - layer_type: str, - input_dimensions: int, - nodes: List[int], - activations: List[str], - initializer_name: str, - replica_seed: int, - dropout: float, - regularizer: str, - regularizer_args: dict, - last_layer_nodes: int, - name: str) -> MetaModel: + layer_type: str, + input_dimensions: int, + nodes: List[int], + activations: List[str], + initializer_name: str, + replica_seed: int, + dropout: float, + regularizer: str, + regularizer_args: dict, + last_layer_nodes: int, + name: str, +) -> MetaModel: """ Create the part of the model that contains all of the actual neural network layers. """ - common_args = {'nodes_in': input_dimensions, 'nodes': nodes, 'activations': activations, 'initializer_name': initializer_name, 'seed': replica_seed} + common_args = { + 'nodes_in': input_dimensions, + 'nodes': nodes, + 'activations': activations, + 'initializer_name': initializer_name, + 'seed': replica_seed, + } if layer_type == "dense": reg = regularizer_selector(regularizer, **regularizer_args) - list_of_pdf_layers = generate_dense_network(**common_args, dropout_rate=dropout, regularizer=reg) + list_of_pdf_layers = generate_dense_network( + **common_args, dropout_rate=dropout, regularizer=reg + ) elif layer_type == "dense_per_flavour": - list_of_pdf_layers = generate_dense_per_flavour_network(**common_args, basis_size=last_layer_nodes) + list_of_pdf_layers = generate_dense_per_flavour_network( + **common_args, basis_size=last_layer_nodes + ) # Note: using a Sequential model would be more appropriate, but it would require # creating a MetaSequential model. @@ -719,4 +733,3 @@ def generate_nn( model = MetaModel({'NN_input': x}, pdf, name=name) return model - diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index 14222f5195..1ad8ce49ac 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -13,12 +13,8 @@ def generate_msr_model_and_grid( - output_dim: int = 14, - mode: str = "ALL", - nx: int = int(2e3), - scaler=None, - photons=None, - **kwargs) -> MetaModel: + output_dim: int = 14, mode: str = "ALL", nx: int = int(2e3), scaler=None, photons=None, **kwargs +) -> MetaModel: """ Generates a model that applies the sum rules to the PDF. @@ -54,7 +50,9 @@ def generate_msr_model_and_grid( """ # 0. Prepare input layers to MSR model pdf_x = Input(shape=(None, output_dim), batch_size=1, name="pdf_x") - pdf_xgrid_integration = Input(shape=(nx, output_dim), batch_size=1, name="pdf_xgrid_integration") + pdf_xgrid_integration = Input( + shape=(nx, output_dim), batch_size=1, name="pdf_xgrid_integration" + ) # 1. Generate the grid and weights that will be used to integrate xgrid_integration, weights_array = gen_integration_input(nx) @@ -66,11 +64,14 @@ def generate_msr_model_and_grid( # so shapes will display properly in the model summary grid_shape = 2 if scaler is not None else 1 xgrid_integration = op.numpy_to_input( - xgrid_integration, name="integration_grid", custom_shape=(None, grid_shape)) + xgrid_integration, name="integration_grid", custom_shape=(None, grid_shape) + ) # 1c Get the original grid if scaler: - get_original = Lambda(lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ") + get_original = Lambda( + lambda x: op.op_gather_keep_dims(x, -1, axis=-1), name="x_original_integ" + ) else: get_original = lambda x: x x_original = get_original(xgrid_integration) @@ -91,20 +92,25 @@ def generate_msr_model_and_grid( # 6. Compute the normalization factor # For now set the photon component to None - normalization_factor = MSR_Normalization(output_dim, mode, name="msr_weights", photons_contribution=photons_c)(pdf_integrated, ph_replica=None) + normalization_factor = MSR_Normalization( + output_dim, mode, name="msr_weights", photons_contribution=photons_c + )(pdf_integrated, ph_replica=None) # 7. Apply the normalization factor to the pdf - pdf_normalized = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized")([pdf_x, normalization_factor]) + pdf_normalized = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized")( + [pdf_x, normalization_factor] + ) inputs = { "pdf_x": pdf_x, "pdf_xgrid_integration": pdf_xgrid_integration, "xgrid_integration": xgrid_integration, - } + } model = MetaModel(inputs, pdf_normalized, name="impose_msr") return model, xgrid_integration + def gen_integration_input(nx): """ Generates a np.array (shaped (nx,1)) of nx elements where the @@ -128,4 +134,3 @@ def gen_integration_input(nx): weights_array = np.array(weights).reshape(nx, 1) return xgrid, weights_array - diff --git a/n3fit/src/n3fit/scaler.py b/n3fit/src/n3fit/scaler.py index 9eac8ed575..b8434a5759 100644 --- a/n3fit/src/n3fit/scaler.py +++ b/n3fit/src/n3fit/scaler.py @@ -52,9 +52,7 @@ def generate_scaler(input_list: List[np.ndarray], interpolation_points: int = No try: scaler = PchipInterpolator(map_from, map_to) except ValueError: - raise ValueError( - "interpolation_points is larger than the number of unique input x-values" - ) + raise ValueError("interpolation_points is larger than the number of unique input x-values") def _scaler(x): x_scaled = scaler(np.log(x)) @@ -62,4 +60,3 @@ def _scaler(x): return np.concatenate([x_scaled, x], axis=-1) return _scaler - diff --git a/n3fit/src/n3fit/tests/test_xops.py b/n3fit/src/n3fit/tests/test_xops.py index cfdd95fc46..8aa2248c91 100644 --- a/n3fit/src/n3fit/tests/test_xops.py +++ b/n3fit/src/n3fit/tests/test_xops.py @@ -18,6 +18,7 @@ def test_xdivide_default(): np.testing.assert_allclose(test_output, expected_output, rtol=1e-05) + def test_xdivide_indices(): """Check that the default xDivide works as expected""" custom_indices = [0, 1, 7] @@ -30,4 +31,3 @@ def test_xdivide_indices(): expected_output[:, :, i] = 1 / test_input[:, :, 0] np.testing.assert_allclose(test_output, expected_output, rtol=1e-05) - From 801366ad65ee0f82a132107f8aa5abee807f59e5 Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 7 Jun 2023 11:41:26 +0200 Subject: [PATCH 49/68] Remove sentence about plotting full model. --- doc/sphinx/source/n3fit/methodology.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/n3fit/methodology.rst b/doc/sphinx/source/n3fit/methodology.rst index 3851bdd16f..78562f323f 100644 --- a/doc/sphinx/source/n3fit/methodology.rst +++ b/doc/sphinx/source/n3fit/methodology.rst @@ -169,7 +169,7 @@ To see the structure of the model, one can use Keras's ``plot_model`` function a This will produce for instance the plot of the PDF model below, and can also be used to plot the -neural network model, the momentum sum rule model, and the model containing everything that is illustrated above. +neural network model, and the momentum sum rule model. .. image:: figures/plot_pdf.png From f99b77b0ac20a4a304933eee555bc35c29d94fc3 Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 14 Jun 2023 12:41:37 +0200 Subject: [PATCH 50/68] Remove now unused scatter_to_zero. --- n3fit/src/n3fit/backends/keras_backend/operations.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index ff3d1a0248..4020466a23 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -361,16 +361,6 @@ def scatter_to_one(values, indices=[[1]], output_dim=14): return tf.tensor_scatter_nd_update(ones, indices, values) -def scatter_to_zero(values, indices, output_dim): - """ - Like scatter_nd initialized to zero - see full `docs `_ - """ - indices = tf.constant([[i] for i in indices]) - updates = tf.constant(values) - return tf.scatter_nd(indices, updates, [output_dim]) - - def op_subtract(inputs, **kwargs): """ Computes the difference between two tensors. From 0bd5582d1788bcc1fbe02db8a2fa92423cc1d369 Mon Sep 17 00:00:00 2001 From: Aron Date: Tue, 20 Jun 2023 16:28:51 +0200 Subject: [PATCH 51/68] Pass replica number as additional argument to impose_msr model --- n3fit/src/n3fit/model_gen.py | 4 ++-- n3fit/src/n3fit/msr.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 33d6ec4e58..959d1929ab 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -676,8 +676,8 @@ def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): pdf_unnormalized = compute_unnormalized_pdf(pdf_input, nn, preprocessing_factor) pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, preprocessing_factor) - pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input]) - + # i_replica argument is necessary to select the right photon integral + pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input, i_replica]) if photons: pdf_normalized = layer_photon(pdf_normalized, i_replica) diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index 1ad8ce49ac..66fb28e4f7 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -85,16 +85,18 @@ def generate_msr_model_and_grid( # 4. Integrate the pdf pdf_integrated = xIntegrator(weights_array, input_shape=(nx,))(pdf_integrand) - # 5. If a photon is given, compute the photon component of the MSR + # 5. If a photon is given, retrieve the photon component of the MSR... photons_c = None if photons: photons_c = photons.integral + # ... and add the replica number as an input, as the above contains the integrals for all replicas + replica_number = Input(shape=(1,), batch_size=1, name="replica_number", dtype="int32") # 6. Compute the normalization factor # For now set the photon component to None normalization_factor = MSR_Normalization( output_dim, mode, name="msr_weights", photons_contribution=photons_c - )(pdf_integrated, ph_replica=None) + )(pdf_integrated, ph_replica=replica_number) # 7. Apply the normalization factor to the pdf pdf_normalized = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized")( @@ -105,6 +107,7 @@ def generate_msr_model_and_grid( "pdf_x": pdf_x, "pdf_xgrid_integration": pdf_xgrid_integration, "xgrid_integration": xgrid_integration, + "replica_number": replica_number, } model = MetaModel(inputs, pdf_normalized, name="impose_msr") From 8dbc98d4fdf2f802316ebdad250549cfd6adf7cb Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 23 Jun 2023 11:08:00 +0200 Subject: [PATCH 52/68] Replace indexing with tf.gather since ph_replica is now a tensor --- n3fit/src/n3fit/backends/keras_backend/operations.py | 3 +++ n3fit/src/n3fit/layers/msr_normalization.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 4020466a23..1007536065 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -193,6 +193,9 @@ def tmp(x): return layer_op(tensor) +def op_gather(tensor, indices, **kwargs): + return tf.gather(tensor, indices, **kwargs) + # # Tensor operations # f(x: tensor[s]) -> y: tensor diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 6725fdbbdf..774164c3c6 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -54,7 +54,7 @@ def call(self, pdf_integrated, ph_replica): norm_constants = [] if self._photons: - photon_integral = self._photons[ph_replica] + photon_integral = op.gather(self._photons, ph_replica) else: photon_integral = 0.0 From a0f6e0927b143d16992aa7d7e9a7ed3543e7541e Mon Sep 17 00:00:00 2001 From: Aron Date: Fri, 23 Jun 2023 12:02:15 +0200 Subject: [PATCH 53/68] gather -> op_gather --- n3fit/src/n3fit/layers/msr_normalization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 774164c3c6..60788a912d 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -54,7 +54,7 @@ def call(self, pdf_integrated, ph_replica): norm_constants = [] if self._photons: - photon_integral = op.gather(self._photons, ph_replica) + photon_integral = op.op_gather(self._photons, ph_replica) else: photon_integral = 0.0 From 20833cae25fd2dfce1e8e2b4edeb0ae32673f47a Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Sat, 24 Jun 2023 07:50:39 +0200 Subject: [PATCH 54/68] Revert to commit 2b8c806e --- n3fit/src/n3fit/backends/keras_backend/operations.py | 3 --- n3fit/src/n3fit/layers/msr_normalization.py | 2 +- n3fit/src/n3fit/model_gen.py | 4 ++-- n3fit/src/n3fit/msr.py | 7 ++----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 1007536065..4020466a23 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -193,9 +193,6 @@ def tmp(x): return layer_op(tensor) -def op_gather(tensor, indices, **kwargs): - return tf.gather(tensor, indices, **kwargs) - # # Tensor operations # f(x: tensor[s]) -> y: tensor diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 60788a912d..6725fdbbdf 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -54,7 +54,7 @@ def call(self, pdf_integrated, ph_replica): norm_constants = [] if self._photons: - photon_integral = op.op_gather(self._photons, ph_replica) + photon_integral = self._photons[ph_replica] else: photon_integral = 0.0 diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 959d1929ab..33d6ec4e58 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -676,8 +676,8 @@ def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): pdf_unnormalized = compute_unnormalized_pdf(pdf_input, nn, preprocessing_factor) pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, preprocessing_factor) - # i_replica argument is necessary to select the right photon integral - pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input, i_replica]) + pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input]) + if photons: pdf_normalized = layer_photon(pdf_normalized, i_replica) diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index 66fb28e4f7..1ad8ce49ac 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -85,18 +85,16 @@ def generate_msr_model_and_grid( # 4. Integrate the pdf pdf_integrated = xIntegrator(weights_array, input_shape=(nx,))(pdf_integrand) - # 5. If a photon is given, retrieve the photon component of the MSR... + # 5. If a photon is given, compute the photon component of the MSR photons_c = None if photons: photons_c = photons.integral - # ... and add the replica number as an input, as the above contains the integrals for all replicas - replica_number = Input(shape=(1,), batch_size=1, name="replica_number", dtype="int32") # 6. Compute the normalization factor # For now set the photon component to None normalization_factor = MSR_Normalization( output_dim, mode, name="msr_weights", photons_contribution=photons_c - )(pdf_integrated, ph_replica=replica_number) + )(pdf_integrated, ph_replica=None) # 7. Apply the normalization factor to the pdf pdf_normalized = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized")( @@ -107,7 +105,6 @@ def generate_msr_model_and_grid( "pdf_x": pdf_x, "pdf_xgrid_integration": pdf_xgrid_integration, "xgrid_integration": xgrid_integration, - "replica_number": replica_number, } model = MetaModel(inputs, pdf_normalized, name="impose_msr") From 6a1ce0287260d8c418cabd00f456f1f9fb3188cd Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Sat, 24 Jun 2023 08:01:21 +0200 Subject: [PATCH 55/68] Set photon integral as explicit argument to normalization model --- n3fit/src/n3fit/layers/msr_normalization.py | 24 +++++++++++++-------- n3fit/src/n3fit/model_gen.py | 15 ++++++++++++- n3fit/src/n3fit/msr.py | 15 ++++++------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/n3fit/src/n3fit/layers/msr_normalization.py b/n3fit/src/n3fit/layers/msr_normalization.py index 6725fdbbdf..552f618b59 100644 --- a/n3fit/src/n3fit/layers/msr_normalization.py +++ b/n3fit/src/n3fit/layers/msr_normalization.py @@ -16,8 +16,7 @@ class MSR_Normalization(MetaLayer): _msr_enabled = False _vsr_enabled = False - def __init__(self, output_dim=14, mode="ALL", photons_contribution=None, **kwargs): - self._photons = photons_contribution + def __init__(self, output_dim=14, mode="ALL", **kwargs): if mode == True or mode.upper() == "ALL": self._msr_enabled = True self._vsr_enabled = True @@ -40,7 +39,7 @@ def __init__(self, output_dim=14, mode="ALL", photons_contribution=None, **kwarg super().__init__(**kwargs) - def call(self, pdf_integrated, ph_replica): + def call(self, pdf_integrated, photon_integral): """Imposes the valence and momentum sum rules: A_g = (1-sigma-photon)/g A_v = A_v24 = A_v35 = 3/V @@ -49,17 +48,24 @@ def call(self, pdf_integrated, ph_replica): A_v15 = 3/V_15 Note that both the input and the output are in the 14-flavours fk-basis + + Parameters + ---------- + pdf_integrated: (Tensor(1,None,14)) + the integrated PDF + photon_integral: (Tensor(1)): + the integrated photon, not included in PDF + + Returns + ------- + normalization_factor: Tensor(14) + The normalization factors per flavour. """ y = op.flatten(pdf_integrated) norm_constants = [] - if self._photons: - photon_integral = self._photons[ph_replica] - else: - photon_integral = 0.0 - if self._msr_enabled: - n_ag = [(1.0 - y[GLUON_IDX[0][0] - 1] - photon_integral) / y[GLUON_IDX[0][0]]] * len( + n_ag = [(1.0 - y[GLUON_IDX[0][0] - 1] - photon_integral[0]) / y[GLUON_IDX[0][0]]] * len( GLUON_IDX ) norm_constants += n_ag diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 33d6ec4e58..ec8d43b43d 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -606,6 +606,14 @@ def pdfNN_layer_generator( model_input["integrator_input"] = integrator_input else: sumrule_layer = lambda x: x + # The photon is treated separately, need to get its integrals to normalize the pdf + if photons: + photon_integrals = photons.integral + else: + photon_integrals = [0.0 for _ in range(len(seed))] + import tensorflow as tf + photon_integrals = [tf.constant(photon_integrals[i_replica]) + for i_replica in range(len(seed))] # Now we need a trainable network per replica to be trained in parallel pdf_models = [] @@ -676,7 +684,12 @@ def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): pdf_unnormalized = compute_unnormalized_pdf(pdf_input, nn, preprocessing_factor) pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, preprocessing_factor) - pdf_normalized = sumrule_layer([pdf_unnormalized, pdf_integration_grid, integrator_input]) + pdf_normalized = sumrule_layer({ + "pdf_x": pdf_unnormalized, + "pdf_xgrid_integration": pdf_integration_grid, + "xgrid_integration": integrator_input, + "photon_integral": photon_integrals[i_replica], + }) if photons: pdf_normalized = layer_photon(pdf_normalized, i_replica) diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index 1ad8ce49ac..e015486df3 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -13,7 +13,7 @@ def generate_msr_model_and_grid( - output_dim: int = 14, mode: str = "ALL", nx: int = int(2e3), scaler=None, photons=None, **kwargs + output_dim: int = 14, mode: str = "ALL", nx: int = int(2e3), scaler=None, **kwargs ) -> MetaModel: """ Generates a model that applies the sum rules to the PDF. @@ -31,8 +31,6 @@ def generate_msr_model_and_grid( Number of points of the integration grid scaler: Scaler Scaler to be applied to the PDF before applying the sum rules - photons: :py:class:`validphys.photon.compute.Photon` - If given, gives the AddPhoton layer a function to compute the MSR component for the photon Returns ------- @@ -42,6 +40,7 @@ def generate_msr_model_and_grid( - pdf_x: the PDF output of the model - pdf_xgrid_integration: the PDF output of the model evaluated at the integration grid - xgrid_integration: the integration grid + - photon_integral: the integrated photon contribution It returns the PDF with the sum rules applied xgrid_integration: dict Dictionary with the integration grid, with: @@ -85,16 +84,13 @@ def generate_msr_model_and_grid( # 4. Integrate the pdf pdf_integrated = xIntegrator(weights_array, input_shape=(nx,))(pdf_integrand) - # 5. If a photon is given, compute the photon component of the MSR - photons_c = None - if photons: - photons_c = photons.integral + # 5. THe input for the photon integral, will be set to 0 if no photons + photon_integral = Input(shape=(), batch_size=1, name='photon_integral') # 6. Compute the normalization factor # For now set the photon component to None normalization_factor = MSR_Normalization( - output_dim, mode, name="msr_weights", photons_contribution=photons_c - )(pdf_integrated, ph_replica=None) + output_dim, mode, name="msr_weights")(pdf_integrated, photon_integral) # 7. Apply the normalization factor to the pdf pdf_normalized = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized")( @@ -105,6 +101,7 @@ def generate_msr_model_and_grid( "pdf_x": pdf_x, "pdf_xgrid_integration": pdf_xgrid_integration, "xgrid_integration": xgrid_integration, + "photon_integral": photon_integral, } model = MetaModel(inputs, pdf_normalized, name="impose_msr") From 6617da2cb8284182eadb1a18b1296bc5f805ec35 Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Wed, 5 Jul 2023 10:14:49 +0200 Subject: [PATCH 56/68] Remove duplicate imports Co-authored-by: Roy Stegeman --- n3fit/src/n3fit/layers/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/n3fit/src/n3fit/layers/__init__.py b/n3fit/src/n3fit/layers/__init__.py index f3e0f7cd54..a9168bda13 100644 --- a/n3fit/src/n3fit/layers/__init__.py +++ b/n3fit/src/n3fit/layers/__init__.py @@ -1,7 +1,3 @@ -from .preprocessing import Preprocessing -from .rotations import FkRotation, FlavourToEvolution, ObsRotation -from .x_operations import xIntegrator, xDivide -from .msr_normalization import MSR_Normalization from n3fit.backends import MetaLayer from .DIS import DIS From 5aa72fc3a0a605fd8ab3938a6032435786141e2b Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Wed, 5 Jul 2023 10:14:58 +0200 Subject: [PATCH 57/68] Remove duplicate imports Co-authored-by: Roy Stegeman --- n3fit/src/n3fit/model_gen.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index ec8d43b43d..d8f3cbb15e 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -14,8 +14,6 @@ import numpy as np from n3fit.msr import generate_msr_model_and_grid -from n3fit.layers import DIS, DY, ObsRotation, losses -from n3fit.layers.observable import is_unique from n3fit.backends import Input, Lambda, MetaLayer, MetaModel, base_layer_selector from n3fit.backends import operations as op From a80a77e4af9d8e3ad3bc828de026431cfdfbced2 Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 5 Jul 2023 10:18:22 +0200 Subject: [PATCH 58/68] Add link to plot_model documentation --- doc/sphinx/source/n3fit/methodology.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/sphinx/source/n3fit/methodology.rst b/doc/sphinx/source/n3fit/methodology.rst index 78562f323f..8380d5526d 100644 --- a/doc/sphinx/source/n3fit/methodology.rst +++ b/doc/sphinx/source/n3fit/methodology.rst @@ -127,7 +127,8 @@ provide a faster convergence to the solution. Parameters like the number of layers, nodes, activation functions are hyper-parameters that require tuning. -To see the structure of the model, one can use Keras's ``plot_model`` function as illustrated in the script below: +To see the structure of the model, one can use Keras's ``plot_model`` function as illustrated in the script below. +See the `Keras documentation `_ for more details. .. code-block:: python From f82b4070df34a52a7fa665caab50bebf07e43a7d Mon Sep 17 00:00:00 2001 From: Aron Date: Wed, 5 Jul 2023 10:40:47 +0200 Subject: [PATCH 59/68] Move photon integrals down to final loop --- n3fit/src/n3fit/model_gen.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index d8f3cbb15e..b46b6b23c0 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -604,14 +604,6 @@ def pdfNN_layer_generator( model_input["integrator_input"] = integrator_input else: sumrule_layer = lambda x: x - # The photon is treated separately, need to get its integrals to normalize the pdf - if photons: - photon_integrals = photons.integral - else: - photon_integrals = [0.0 for _ in range(len(seed))] - import tensorflow as tf - photon_integrals = [tf.constant(photon_integrals[i_replica]) - for i_replica in range(len(seed))] # Now we need a trainable network per replica to be trained in parallel pdf_models = [] @@ -686,13 +678,15 @@ def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): "pdf_x": pdf_unnormalized, "pdf_xgrid_integration": pdf_integration_grid, "xgrid_integration": integrator_input, - "photon_integral": photon_integrals[i_replica], + # The photon is treated separately, need to get its integrals to normalize the pdf + "photon_integral": op.numpy_to_tensor(0.0 if not photons else photons.integral[i_replica]), }) if photons: - pdf_normalized = layer_photon(pdf_normalized, i_replica) - - model_output = pdf_normalized + # Add in the photon component + model_output = layer_photon(pdf_normalized, i_replica) + else: + model_output = pdf_normalized # Create the model pdf_model = MetaModel(model_input, model_output, name=f"PDF_{i_replica}", scaler=scaler) From 80ef5d72a5bf1c2476ba1f0bd1374b2d7239c700 Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Wed, 5 Jul 2023 13:25:12 +0200 Subject: [PATCH 60/68] Simplify numpy_to_input, removing unused no_reshape and setting gridpoints to None in shape always --- .../backends/keras_backend/operations.py | 23 +++++-------------- n3fit/src/n3fit/model_gen.py | 4 +--- n3fit/src/n3fit/msr.py | 8 ++----- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 4020466a23..77c45bdfae 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -114,9 +114,7 @@ def batchit(x, batch_dimension=0, **kwarg): # layer generation def numpy_to_input( numpy_array: np.ndarray, - no_reshape: bool = False, name: str = None, - custom_shape: tuple = None, ): """ Takes a numpy array and generates a Input layer. @@ -126,26 +124,17 @@ def numpy_to_input( Parameters ---------- numpy_array: np.ndarray - no_reshape: bool - if true, don't add batch dimension, take the first dimension of the array as the batch name: bool name to give to the layer - custom_shape: tuple - To specify a more general shape with None values """ - if no_reshape: - batched_array = numpy_array - batch_size = numpy_array.shape[0] - shape = numpy_array.shape[1:] - else: - batched_array = np.expand_dims(numpy_array, 0) - batch_size = 1 - shape = numpy_array.shape - if custom_shape is not None: - shape = custom_shape + batched_array = np.expand_dims(numpy_array, 0) + batch_size = 1 + shape = list(numpy_array.shape) + # set the number of gridpoints to None, otherwise shapes don't show in model.summary + shape[0] = None + input_layer = Input(batch_size=batch_size, shape=shape, name=name) input_layer.tensor_content = batched_array - input_layer.original_shape = no_reshape return input_layer diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index b46b6b23c0..4262887937 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -577,9 +577,7 @@ def pdfNN_layer_generator( input_x_eq_1 = scaler(input_x_eq_1)[0] # the layer that subtracts 1 from the NN output subtract_one_layer = Lambda(op.op_subtract, name='subtract_one') - layer_x_eq_1 = op.numpy_to_input( - np.array(input_x_eq_1).reshape(1, 1), name='x_ones', custom_shape=(None, 1) - ) # Just to make shapes consistent + layer_x_eq_1 = op.numpy_to_input(np.array(input_x_eq_1).reshape(1, 1), name='x_eq_1') model_input["layer_x_eq_1"] = layer_x_eq_1 # the layer that multiplies the NN output by the preprocessing factor diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index e015486df3..c74450676a 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -59,12 +59,8 @@ def generate_msr_model_and_grid( if scaler: xgrid_integration = scaler(xgrid_integration) - # Turn into input layer. Specify a custom shape here using None, - # so shapes will display properly in the model summary - grid_shape = 2 if scaler is not None else 1 - xgrid_integration = op.numpy_to_input( - xgrid_integration, name="integration_grid", custom_shape=(None, grid_shape) - ) + # Turn into input layer. + xgrid_integration = op.numpy_to_input(xgrid_integration, name="integration_grid") # 1c Get the original grid if scaler: From 0a4d684cd148d6d4d63a11872bc9372d8744b890 Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Fri, 7 Jul 2023 09:08:11 +0200 Subject: [PATCH 61/68] Improve numpy_to_input documentation --- n3fit/src/n3fit/backends/keras_backend/operations.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 77c45bdfae..a53e3f3e71 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -117,23 +117,21 @@ def numpy_to_input( name: str = None, ): """ - Takes a numpy array and generates a Input layer. - By default it adds a batch dimension (of size 1) so that the shape of the layer - is that of the array + Takes a numpy array and generates an Input layer with the same shape, + but with a batch dimension (of size 1) added. Parameters ---------- numpy_array: np.ndarray - name: bool + name: str name to give to the layer """ batched_array = np.expand_dims(numpy_array, 0) - batch_size = 1 shape = list(numpy_array.shape) # set the number of gridpoints to None, otherwise shapes don't show in model.summary shape[0] = None - input_layer = Input(batch_size=batch_size, shape=shape, name=name) + input_layer = Input(batch_size=1, shape=shape, name=name) input_layer.tensor_content = batched_array return input_layer From 0518c7cc6f133d1fe91d0dba840bc3d347684dc6 Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Mon, 10 Jul 2023 14:10:36 +0200 Subject: [PATCH 62/68] Apply black and isort to all affected files --- .../backends/keras_backend/operations.py | 8 +++---- n3fit/src/n3fit/layers/preprocessing.py | 10 ++------ n3fit/src/n3fit/layers/x_operations.py | 3 ++- n3fit/src/n3fit/model_gen.py | 23 +++++++++++-------- n3fit/src/n3fit/model_trainer.py | 12 +++------- n3fit/src/n3fit/msr.py | 7 +++--- n3fit/src/n3fit/scaler.py | 1 + n3fit/src/n3fit/tests/test_xops.py | 1 + 8 files changed, 29 insertions(+), 36 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index a53e3f3e71..37097ed00f 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -25,13 +25,12 @@ import numpy as np import tensorflow as tf +from tensorflow.keras import backend as K +from tensorflow.keras.layers import Input from tensorflow.keras.layers import Lambda as keras_Lambda from tensorflow.keras.layers import multiply as keras_multiply from tensorflow.keras.layers import subtract as keras_subtract -from tensorflow.keras.layers import Input -from tensorflow.keras import backend as K - from validphys.convolution import OP @@ -113,8 +112,7 @@ def batchit(x, batch_dimension=0, **kwarg): # layer generation def numpy_to_input( - numpy_array: np.ndarray, - name: str = None, + numpy_array: np.ndarray, name: str = None, ): """ Takes a numpy array and generates an Input layer with the same shape, diff --git a/n3fit/src/n3fit/layers/preprocessing.py b/n3fit/src/n3fit/layers/preprocessing.py index 3aefbadaec..e5d45c7038 100644 --- a/n3fit/src/n3fit/layers/preprocessing.py +++ b/n3fit/src/n3fit/layers/preprocessing.py @@ -1,5 +1,4 @@ -from n3fit.backends import MetaLayer -from n3fit.backends import constraints +from n3fit.backends import MetaLayer, constraints from n3fit.backends import operations as op @@ -35,12 +34,7 @@ class Preprocessing(MetaLayer): """ def __init__( - self, - flav_info=None, - seed=0, - initializer="random_uniform", - large_x=True, - **kwargs, + self, flav_info=None, seed=0, initializer="random_uniform", large_x=True, **kwargs, ): if flav_info is None: raise ValueError( diff --git a/n3fit/src/n3fit/layers/x_operations.py b/n3fit/src/n3fit/layers/x_operations.py index c56a369a75..d8a3d47b3d 100644 --- a/n3fit/src/n3fit/layers/x_operations.py +++ b/n3fit/src/n3fit/layers/x_operations.py @@ -14,7 +14,8 @@ from n3fit.backends import MetaLayer from n3fit.backends import operations as op -BASIS_SIZE=14 +BASIS_SIZE = 14 + class xDivide(MetaLayer): """ diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 4262887937..55eba95447 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -9,11 +9,10 @@ """ -from typing import List, Callable from dataclasses import dataclass +from typing import Callable, List import numpy as np -from n3fit.msr import generate_msr_model_and_grid from n3fit.backends import Input, Lambda, MetaLayer, MetaModel, base_layer_selector from n3fit.backends import operations as op @@ -29,7 +28,7 @@ losses, ) from n3fit.layers.observable import is_unique - +from n3fit.msr import generate_msr_model_and_grid from validphys.photon.compute import Photon # only used for type hint here @@ -672,13 +671,17 @@ def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): pdf_unnormalized = compute_unnormalized_pdf(pdf_input, nn, preprocessing_factor) pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, preprocessing_factor) - pdf_normalized = sumrule_layer({ - "pdf_x": pdf_unnormalized, - "pdf_xgrid_integration": pdf_integration_grid, - "xgrid_integration": integrator_input, - # The photon is treated separately, need to get its integrals to normalize the pdf - "photon_integral": op.numpy_to_tensor(0.0 if not photons else photons.integral[i_replica]), - }) + pdf_normalized = sumrule_layer( + { + "pdf_x": pdf_unnormalized, + "pdf_xgrid_integration": pdf_integration_grid, + "xgrid_integration": integrator_input, + # The photon is treated separately, need to get its integrals to normalize the pdf + "photon_integral": op.numpy_to_tensor( + 0.0 if not photons else photons.integral[i_replica] + ), + } + ) if photons: # Add in the photon component diff --git a/n3fit/src/n3fit/model_trainer.py b/n3fit/src/n3fit/model_trainer.py index 974dd18b16..2f77590a6e 100644 --- a/n3fit/src/n3fit/model_trainer.py +++ b/n3fit/src/n3fit/model_trainer.py @@ -16,10 +16,10 @@ from n3fit import model_gen from n3fit.backends import MetaModel, callbacks, clear_backend_state -from n3fit.scaler import generate_scaler from n3fit.backends import operations as op import n3fit.hyper_optimization.penalties import n3fit.hyper_optimization.rewards +from n3fit.scaler import generate_scaler from n3fit.stopping import Stopping from n3fit.vpinterface import N3PDF from validphys.photon.compute import Photon @@ -846,9 +846,7 @@ def hyperparametrizable(self, params): # Initialize all photon classes for the different replicas: if self.lux_params: photons = Photon( - theoryid=self.theoryid, - lux_params=self.lux_params, - replicas=self.replicas, + theoryid=self.theoryid, lux_params=self.lux_params, replicas=self.replicas, ) else: photons = None @@ -920,11 +918,7 @@ def hyperparametrizable(self, params): for model in models.values(): model.compile(**params["optimizer"]) - passed = self._train_and_fit( - models["training"], - stopping_object, - epochs=epochs, - ) + passed = self._train_and_fit(models["training"], stopping_object, epochs=epochs,) if self.mode_hyperopt: # If doing a hyperparameter scan we need to keep track of the loss function diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index c74450676a..b7f86fc591 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -5,8 +5,8 @@ import numpy as np +from n3fit.backends import Input, Lambda, MetaModel from n3fit.backends import operations as op -from n3fit.backends import MetaModel, Lambda, Input from n3fit.layers import MSR_Normalization, xDivide, xIntegrator log = logging.getLogger(__name__) @@ -85,8 +85,9 @@ def generate_msr_model_and_grid( # 6. Compute the normalization factor # For now set the photon component to None - normalization_factor = MSR_Normalization( - output_dim, mode, name="msr_weights")(pdf_integrated, photon_integral) + normalization_factor = MSR_Normalization(output_dim, mode, name="msr_weights")( + pdf_integrated, photon_integral + ) # 7. Apply the normalization factor to the pdf pdf_normalized = Lambda(lambda pdf_norm: pdf_norm[0] * pdf_norm[1], name="pdf_normalized")( diff --git a/n3fit/src/n3fit/scaler.py b/n3fit/src/n3fit/scaler.py index b8434a5759..f4f5abff72 100644 --- a/n3fit/src/n3fit/scaler.py +++ b/n3fit/src/n3fit/scaler.py @@ -1,4 +1,5 @@ from typing import Callable, List + import numpy as np from scipy.interpolate import PchipInterpolator diff --git a/n3fit/src/n3fit/tests/test_xops.py b/n3fit/src/n3fit/tests/test_xops.py index 8aa2248c91..99ed67f64e 100644 --- a/n3fit/src/n3fit/tests/test_xops.py +++ b/n3fit/src/n3fit/tests/test_xops.py @@ -2,6 +2,7 @@ Test the x operations """ import numpy as np + from n3fit.layers import xDivide From e92b706e6c966bd71b7482ac0c6ca44614316f45 Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Mon, 10 Jul 2023 15:19:48 +0200 Subject: [PATCH 63/68] Update n3fit/src/n3fit/backends/keras_backend/operations.py formatting Co-authored-by: Juan M. Cruz-Martinez --- n3fit/src/n3fit/backends/keras_backend/operations.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 37097ed00f..41cc1b84a9 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -111,9 +111,7 @@ def batchit(x, batch_dimension=0, **kwarg): # layer generation -def numpy_to_input( - numpy_array: np.ndarray, name: str = None, -): +def numpy_to_input(numpy_array: np.ndarray, name: str = None): """ Takes a numpy array and generates an Input layer with the same shape, but with a batch dimension (of size 1) added. From 5bcf1694cca62bf0b5ae9b2781c45dc29060bab1 Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Mon, 10 Jul 2023 15:33:58 +0200 Subject: [PATCH 64/68] Restore comments in scaler --- n3fit/src/n3fit/scaler.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/n3fit/src/n3fit/scaler.py b/n3fit/src/n3fit/scaler.py index f4f5abff72..0a8be1f91c 100644 --- a/n3fit/src/n3fit/scaler.py +++ b/n3fit/src/n3fit/scaler.py @@ -23,6 +23,9 @@ def generate_scaler(input_list: List[np.ndarray], interpolation_points: int = No input_arr = np.sort(input_arr) input_arr_size = input_arr.size + # Define an evenly spaced grid in the domain [0,1] + # force_set_smallest is used to make sure the smallest point included in the scaling is + # 1e-9, to prevent trouble when saving it to the LHAPDF grid force_set_smallest = input_arr.min() > 1e-9 if force_set_smallest: new_xgrid = np.linspace( @@ -31,17 +34,23 @@ def generate_scaler(input_list: List[np.ndarray], interpolation_points: int = No else: new_xgrid = np.linspace(start=0, stop=1.0, endpoint=False, num=input_arr_size) + # When mapping the FK xgrids onto our new grid, we need to consider degeneracies among + # the x-values in the FK grids unique, counts = np.unique(input_arr, return_counts=True) map_to_complete = [] for cumsum_ in np.cumsum(counts): + # Make sure to include the smallest new_xgrid value, such that we have a point at + # x<=1e-9 map_to_complete.append(new_xgrid[cumsum_ - counts[0]]) map_to_complete = np.array(map_to_complete) map_from_complete = unique + # If needed, set feature_scaling(x=1e-9)=0 if force_set_smallest: map_from_complete = np.insert(map_from_complete, 0, 1e-9) map_to_complete = np.insert(map_to_complete, 0, 0.0) + # Select the indices of the points that will be used by the interpolator onein = map_from_complete.size / (int(interpolation_points) - 1) selected_points = [round(i * onein - 1) for i in range(1, int(interpolation_points))] if selected_points[0] != 0: From 1a9ce3038db0121ab7bb52f028187b085cff66b7 Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Wed, 12 Jul 2023 10:31:12 +0200 Subject: [PATCH 65/68] Add Optional type hint for arguments that can be None --- n3fit/src/n3fit/backends/keras_backend/operations.py | 4 +++- n3fit/src/n3fit/layers/x_operations.py | 6 ++++-- n3fit/src/n3fit/msr.py | 7 ++++++- n3fit/src/n3fit/scaler.py | 8 +++++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 41cc1b84a9..677e8aacba 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -23,6 +23,8 @@ equally operations are automatically converted to layers when used as such. """ +from typing import Optional + import numpy as np import tensorflow as tf from tensorflow.keras import backend as K @@ -111,7 +113,7 @@ def batchit(x, batch_dimension=0, **kwarg): # layer generation -def numpy_to_input(numpy_array: np.ndarray, name: str = None): +def numpy_to_input(numpy_array: np.ndarray, name: Optional[str] = None): """ Takes a numpy array and generates an Input layer with the same shape, but with a batch dimension (of size 1) added. diff --git a/n3fit/src/n3fit/layers/x_operations.py b/n3fit/src/n3fit/layers/x_operations.py index d8a3d47b3d..a71a42d93d 100644 --- a/n3fit/src/n3fit/layers/x_operations.py +++ b/n3fit/src/n3fit/layers/x_operations.py @@ -9,7 +9,7 @@ for all flavours. The choice of flavours on which to act in a different way is given as an input argument. """ -from typing import List +from typing import List, Optional from n3fit.backends import MetaLayer from n3fit.backends import operations as op @@ -34,7 +34,9 @@ class xDivide(MetaLayer): list of indices to be divided by X (by default [3, 4, 5, 6]; [v, v3, v8, v15] """ - def __init__(self, output_dim: int = BASIS_SIZE, div_list: List = None, **kwargs): + def __init__( + self, output_dim: int = BASIS_SIZE, div_list: Optional[List[int]] = None, **kwargs + ): if div_list is None: div_list = [3, 4, 5, 6] self.output_dim = output_dim diff --git a/n3fit/src/n3fit/msr.py b/n3fit/src/n3fit/msr.py index b7f86fc591..17392d3022 100644 --- a/n3fit/src/n3fit/msr.py +++ b/n3fit/src/n3fit/msr.py @@ -2,6 +2,7 @@ The constraint module include functions to impose the momentum sum rules on the PDFs """ import logging +from typing import Callable, Optional import numpy as np @@ -13,7 +14,11 @@ def generate_msr_model_and_grid( - output_dim: int = 14, mode: str = "ALL", nx: int = int(2e3), scaler=None, **kwargs + output_dim: int = 14, + mode: str = "ALL", + nx: int = int(2e3), + scaler: Optional[Callable] = None, + **kwargs ) -> MetaModel: """ Generates a model that applies the sum rules to the PDF. diff --git a/n3fit/src/n3fit/scaler.py b/n3fit/src/n3fit/scaler.py index 0a8be1f91c..50a6b2d80d 100644 --- a/n3fit/src/n3fit/scaler.py +++ b/n3fit/src/n3fit/scaler.py @@ -1,10 +1,12 @@ -from typing import Callable, List +from typing import Callable, List, Optional import numpy as np from scipy.interpolate import PchipInterpolator -def generate_scaler(input_list: List[np.ndarray], interpolation_points: int = None) -> Callable: +def generate_scaler( + input_list: List[np.ndarray], interpolation_points: Optional[int] = None +) -> Callable: """ Generate the scaler function that applies feature scaling to the input data. @@ -16,7 +18,7 @@ def generate_scaler(input_list: List[np.ndarray], interpolation_points: int = No Returns ------- - _scaler : function + _scaler : Callable The scaler function that applies feature scaling to the input data. """ input_arr = np.concatenate(input_list, axis=1) From 9b6a675587c6ffd407f859a962d2023c117fb81b Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Wed, 12 Jul 2023 10:41:14 +0200 Subject: [PATCH 66/68] Use numpy.typing --- n3fit/src/n3fit/backends/keras_backend/operations.py | 3 ++- n3fit/src/n3fit/scaler.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/n3fit/src/n3fit/backends/keras_backend/operations.py b/n3fit/src/n3fit/backends/keras_backend/operations.py index 677e8aacba..efff803b92 100644 --- a/n3fit/src/n3fit/backends/keras_backend/operations.py +++ b/n3fit/src/n3fit/backends/keras_backend/operations.py @@ -26,6 +26,7 @@ from typing import Optional import numpy as np +import numpy.typing as npt import tensorflow as tf from tensorflow.keras import backend as K from tensorflow.keras.layers import Input @@ -113,7 +114,7 @@ def batchit(x, batch_dimension=0, **kwarg): # layer generation -def numpy_to_input(numpy_array: np.ndarray, name: Optional[str] = None): +def numpy_to_input(numpy_array: npt.NDArray, name: Optional[str] = None): """ Takes a numpy array and generates an Input layer with the same shape, but with a batch dimension (of size 1) added. diff --git a/n3fit/src/n3fit/scaler.py b/n3fit/src/n3fit/scaler.py index 50a6b2d80d..c52a5718d6 100644 --- a/n3fit/src/n3fit/scaler.py +++ b/n3fit/src/n3fit/scaler.py @@ -1,11 +1,12 @@ from typing import Callable, List, Optional import numpy as np +import numpy.typing as npt from scipy.interpolate import PchipInterpolator def generate_scaler( - input_list: List[np.ndarray], interpolation_points: Optional[int] = None + input_list: List[npt.NDArray], interpolation_points: Optional[int] = None ) -> Callable: """ Generate the scaler function that applies feature scaling to the input data. From 8566728017776b0c083f67f40370db618d58f647 Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Wed, 12 Jul 2023 10:43:30 +0200 Subject: [PATCH 67/68] Raise error from original error --- n3fit/src/n3fit/scaler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/n3fit/src/n3fit/scaler.py b/n3fit/src/n3fit/scaler.py index c52a5718d6..2129f24d7f 100644 --- a/n3fit/src/n3fit/scaler.py +++ b/n3fit/src/n3fit/scaler.py @@ -64,8 +64,10 @@ def generate_scaler( try: scaler = PchipInterpolator(map_from, map_to) - except ValueError: - raise ValueError("interpolation_points is larger than the number of unique input x-values") + except ValueError as e: + raise ValueError( + "interpolation_points is larger than the number of unique input x-values" + ) from e def _scaler(x): x_scaled = scaler(np.log(x)) From 27f9357e2862e34670e35394cc635167f7def78b Mon Sep 17 00:00:00 2001 From: Aron Jansen Date: Wed, 12 Jul 2023 11:17:07 +0200 Subject: [PATCH 68/68] Fix error when not imposing sumrule --- n3fit/src/n3fit/model_gen.py | 38 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/n3fit/src/n3fit/model_gen.py b/n3fit/src/n3fit/model_gen.py index 55eba95447..2bd898021b 100644 --- a/n3fit/src/n3fit/model_gen.py +++ b/n3fit/src/n3fit/model_gen.py @@ -669,28 +669,32 @@ def compute_unnormalized_pdf(x, neural_network, compute_preprocessing_factor): zip(preprocessing_factor_replicas, nn_replicas) ): pdf_unnormalized = compute_unnormalized_pdf(pdf_input, nn, preprocessing_factor) - pdf_integration_grid = compute_unnormalized_pdf(integrator_input, nn, preprocessing_factor) - - pdf_normalized = sumrule_layer( - { - "pdf_x": pdf_unnormalized, - "pdf_xgrid_integration": pdf_integration_grid, - "xgrid_integration": integrator_input, - # The photon is treated separately, need to get its integrals to normalize the pdf - "photon_integral": op.numpy_to_tensor( - 0.0 if not photons else photons.integral[i_replica] - ), - } - ) + + if impose_sumrule: + pdf_integration_grid = compute_unnormalized_pdf( + integrator_input, nn, preprocessing_factor + ) + pdf_normalized = sumrule_layer( + { + "pdf_x": pdf_unnormalized, + "pdf_xgrid_integration": pdf_integration_grid, + "xgrid_integration": integrator_input, + # The photon is treated separately, need to get its integrals to normalize the pdf + "photon_integral": op.numpy_to_tensor( + 0.0 if not photons else photons.integral[i_replica] + ), + } + ) + pdf = pdf_normalized + else: + pdf = pdf_unnormalized if photons: # Add in the photon component - model_output = layer_photon(pdf_normalized, i_replica) - else: - model_output = pdf_normalized + pdf = layer_photon(pdf, i_replica) # Create the model - pdf_model = MetaModel(model_input, model_output, name=f"PDF_{i_replica}", scaler=scaler) + pdf_model = MetaModel(model_input, pdf, name=f"PDF_{i_replica}", scaler=scaler) pdf_models.append(pdf_model) return pdf_models