From df0248e021d57302288ae30caf685ec5c901f12b Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Mon, 27 Apr 2026 18:11:55 +0100 Subject: [PATCH 01/11] add Tikhonov regularisation argument for use in emulated VC matrix inversion --- freegsnke/control_loop/pcs.py | 6 ++++++ freegsnke/control_loop/virtual_circuits_category.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/freegsnke/control_loop/pcs.py b/freegsnke/control_loop/pcs.py index 7464dfc5..bf3b0dab 100644 --- a/freegsnke/control_loop/pcs.py +++ b/freegsnke/control_loop/pcs.py @@ -184,6 +184,7 @@ def calculate_ctrl_voltages( emulator_coils_calc=None, emu_inputs=None, vc_update_rate=None, + tikhonov_lambda=None, verbose=False, ): """ @@ -249,6 +250,10 @@ def calculate_ctrl_voltages( emulator_coils_calc : list of str, optional List of coils to use in emulated VC compuation. These are coils to use in computing shape sensitivity matrix. + tikhonov_lambda : numpy.ndarray , optional + Array of regularisation values for Tikhonov regularisation in emualted VC matrix inversion. + Must be same length as emulator_coils_calc. + verbose : bool, optional If True, prints diagnostic information from subsystem controllers. @@ -322,6 +327,7 @@ def calculate_ctrl_voltages( emulated_VC_targets_calc=emulated_VC_targets_calc, emulator_coils_calc=emulator_coils_calc, emu_inputs=emu_inputs, + tikhonov_lambda=tikhonov_lambda, ) ) diff --git a/freegsnke/control_loop/virtual_circuits_category.py b/freegsnke/control_loop/virtual_circuits_category.py index 3ef35ee3..4fa4d9fc 100644 --- a/freegsnke/control_loop/virtual_circuits_category.py +++ b/freegsnke/control_loop/virtual_circuits_category.py @@ -156,6 +156,7 @@ def run_control( emulated_VC_targets_calc=None, emulator_coils_calc=None, emu_inputs=None, + tikhonov_lambda=None, verbose=False, ): """ @@ -253,6 +254,7 @@ def run_control( coils=self.ctrl_coils, coils_calc=emulator_coils_calc, input_data=emu_inputs, + tikhonov_lambda=tikhonov_lambda, ) # update latest vcs/times self.latest_vc_time = 1.0 * t @@ -272,6 +274,7 @@ def run_control( coils=self.ctrl_coils, coils_calc=emulator_coils_calc, input_data=emu_inputs, + tikhonov_lambda=tikhonov_lambda, ) # update latest VCs and times From 062f801d4258ad748c09fcb79b3c1f11e0a9728a Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Mon, 27 Apr 2026 18:12:23 +0100 Subject: [PATCH 02/11] remove unused variable in calculate_ctrl_voltages --- freegsnke/control_loop/pcs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freegsnke/control_loop/pcs.py b/freegsnke/control_loop/pcs.py index bf3b0dab..9baed649 100644 --- a/freegsnke/control_loop/pcs.py +++ b/freegsnke/control_loop/pcs.py @@ -183,7 +183,6 @@ def calculate_ctrl_voltages( emulated_VC_targets_calc=None, emulator_coils_calc=None, emu_inputs=None, - vc_update_rate=None, tikhonov_lambda=None, verbose=False, ): From 8ef13568706daf6344d10ff895f11c0cacfab669 Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Tue, 28 Apr 2026 15:41:59 +0100 Subject: [PATCH 03/11] update docstring with tikhonov_lambda input --- freegsnke/control_loop/virtual_circuits_category.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freegsnke/control_loop/virtual_circuits_category.py b/freegsnke/control_loop/virtual_circuits_category.py index 4fa4d9fc..5634164e 100644 --- a/freegsnke/control_loop/virtual_circuits_category.py +++ b/freegsnke/control_loop/virtual_circuits_category.py @@ -200,6 +200,10 @@ def run_control( emu_inputs : np.ndarray , optional Array of input values for all input parameters (currents and other plasma parameters) of the Neural Network emulator. + tikhonov_lambda : numpy.ndarray , optional + Array of regularisation values for Tikhonov regularisation in emualted VC matrix inversion. + Must be same length as emulator_coils_calc. + verbose : bool Print some output if True. From 7d34ea60d8a58f13882b6f417f70b12d41591e75 Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Mon, 27 Apr 2026 18:11:55 +0100 Subject: [PATCH 04/11] add Tikhonov regularisation argument for use in emulated VC matrix inversion --- freegsnke/control_loop/pcs.py | 6 ++++++ freegsnke/control_loop/virtual_circuits_category.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/freegsnke/control_loop/pcs.py b/freegsnke/control_loop/pcs.py index 7464dfc5..bf3b0dab 100644 --- a/freegsnke/control_loop/pcs.py +++ b/freegsnke/control_loop/pcs.py @@ -184,6 +184,7 @@ def calculate_ctrl_voltages( emulator_coils_calc=None, emu_inputs=None, vc_update_rate=None, + tikhonov_lambda=None, verbose=False, ): """ @@ -249,6 +250,10 @@ def calculate_ctrl_voltages( emulator_coils_calc : list of str, optional List of coils to use in emulated VC compuation. These are coils to use in computing shape sensitivity matrix. + tikhonov_lambda : numpy.ndarray , optional + Array of regularisation values for Tikhonov regularisation in emualted VC matrix inversion. + Must be same length as emulator_coils_calc. + verbose : bool, optional If True, prints diagnostic information from subsystem controllers. @@ -322,6 +327,7 @@ def calculate_ctrl_voltages( emulated_VC_targets_calc=emulated_VC_targets_calc, emulator_coils_calc=emulator_coils_calc, emu_inputs=emu_inputs, + tikhonov_lambda=tikhonov_lambda, ) ) diff --git a/freegsnke/control_loop/virtual_circuits_category.py b/freegsnke/control_loop/virtual_circuits_category.py index 3ef35ee3..4fa4d9fc 100644 --- a/freegsnke/control_loop/virtual_circuits_category.py +++ b/freegsnke/control_loop/virtual_circuits_category.py @@ -156,6 +156,7 @@ def run_control( emulated_VC_targets_calc=None, emulator_coils_calc=None, emu_inputs=None, + tikhonov_lambda=None, verbose=False, ): """ @@ -253,6 +254,7 @@ def run_control( coils=self.ctrl_coils, coils_calc=emulator_coils_calc, input_data=emu_inputs, + tikhonov_lambda=tikhonov_lambda, ) # update latest vcs/times self.latest_vc_time = 1.0 * t @@ -272,6 +274,7 @@ def run_control( coils=self.ctrl_coils, coils_calc=emulator_coils_calc, input_data=emu_inputs, + tikhonov_lambda=tikhonov_lambda, ) # update latest VCs and times From a5f6ee0e2f55034f69f3ae14fa25c4c6c41b5e6b Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Mon, 27 Apr 2026 18:12:23 +0100 Subject: [PATCH 05/11] remove unused variable in calculate_ctrl_voltages --- freegsnke/control_loop/pcs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freegsnke/control_loop/pcs.py b/freegsnke/control_loop/pcs.py index bf3b0dab..9baed649 100644 --- a/freegsnke/control_loop/pcs.py +++ b/freegsnke/control_loop/pcs.py @@ -183,7 +183,6 @@ def calculate_ctrl_voltages( emulated_VC_targets_calc=None, emulator_coils_calc=None, emu_inputs=None, - vc_update_rate=None, tikhonov_lambda=None, verbose=False, ): From f0b2e632290b08a9cc603f1c48c31711a9989afc Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Tue, 28 Apr 2026 15:41:59 +0100 Subject: [PATCH 06/11] update docstring with tikhonov_lambda input --- freegsnke/control_loop/virtual_circuits_category.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freegsnke/control_loop/virtual_circuits_category.py b/freegsnke/control_loop/virtual_circuits_category.py index 4fa4d9fc..5634164e 100644 --- a/freegsnke/control_loop/virtual_circuits_category.py +++ b/freegsnke/control_loop/virtual_circuits_category.py @@ -200,6 +200,10 @@ def run_control( emu_inputs : np.ndarray , optional Array of input values for all input parameters (currents and other plasma parameters) of the Neural Network emulator. + tikhonov_lambda : numpy.ndarray , optional + Array of regularisation values for Tikhonov regularisation in emualted VC matrix inversion. + Must be same length as emulator_coils_calc. + verbose : bool Print some output if True. From d103ff808ac40c117abe61a70dce99db015bbcd8 Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Tue, 28 Apr 2026 17:12:47 +0100 Subject: [PATCH 07/11] update to jacobian/vc storage --- freegsnke/control_loop/virtual_circuits_category.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freegsnke/control_loop/virtual_circuits_category.py b/freegsnke/control_loop/virtual_circuits_category.py index 5634164e..6e6c369e 100644 --- a/freegsnke/control_loop/virtual_circuits_category.py +++ b/freegsnke/control_loop/virtual_circuits_category.py @@ -286,8 +286,9 @@ def run_control( self.latest_vc = VC_shape_emu # store sensitivity matrix (Jacobian) - self.emulated_jacobian_list.append(self.vc_generator.jacobian_matrix) - self.emulated_vc_list.append(self.vc_generator.vc_matrix) + # self.emulated_jacobian_list.append(self.vc_generator.jacobian_matrix) + # self.emulated_vc_list.append(self.vc_generator.vc_matrix) + self.emulated_vc_list.append(self.latest_vc) self.emulated_vc_times.append(t) else: From 334587cba30b7e99f63b95af0100f7689b038e87 Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Wed, 29 Apr 2026 10:19:52 +0100 Subject: [PATCH 08/11] add optional tikhonov regularisation to inverse calculation of VC matrix --- freegsnke/virtual_circuits.py | 75 +++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/freegsnke/virtual_circuits.py b/freegsnke/virtual_circuits.py index 73358b93..efccd951 100644 --- a/freegsnke/virtual_circuits.py +++ b/freegsnke/virtual_circuits.py @@ -314,6 +314,71 @@ def build_dIydI_j( return dtargets / final_dI + @staticmethod + def calculate_matrix_inverse( + matrix: np.ndarray, tikhonov_lambda: np.ndarray = None + ): + """ + Compute inverse of a generically non-square matrix + By default Moore Penrose inverse is used (np.pinv). + If Tikhonov_lambda is provided then Tikhonov regularisation is applied + and inv = [M^T M + diag(lambda)]^-1 M^T + + Inputs: + ------- + matrix : np.ndarray + matrix to be inverted + + tikhonov_lambda : np.ndarray, optional + 1d array of tikhonov coefficients, or 2d diagonal matrix of coefficients. + Must have size/shape consistent with matrix.shape[1]. + + + Returns: + -------- + inverse : np.ndarray + inverse of matrix + """ + matrix = np.asarray(matrix) # convert tensorflow to numpy. + if tikhonov_lambda is None: + # use regular moore-penrose pseudo inverse + print("Computing Moore-Penrose pseudoinverse") + inverse = np.linalg.pinv(matrix) + + else: + print("Computing Tikhonov regularised inverse") + tikhonov_lambda = np.asarray(tikhonov_lambda) + n_cols = matrix.shape[1] + + if tikhonov_lambda.ndim == 1: + if tikhonov_lambda.shape[0] != n_cols: + raise ValueError( + f"tikhonov_lambda length {tikhonov_lambda.shape[0]} " + f"must match matrix column count {n_cols}." + ) + tikhonov_matrix = np.diag(tikhonov_lambda) + + elif tikhonov_lambda.ndim == 2: + if tikhonov_lambda.shape != (n_cols, n_cols): + raise ValueError( + f"tikhonov_lambda shape {tikhonov_lambda.shape} " + f"must be ({n_cols}, {n_cols}) to match matrix column count." + ) + if not np.allclose(tikhonov_lambda, np.diag(np.diag(tikhonov_lambda))): + raise ValueError( + "tikhonov_lambda 2d array must be a diagonal matrix." + ) + tikhonov_matrix = tikhonov_lambda + + else: + raise ValueError( + f"tikhonov_lambda must be 1d or 2d, got {tikhonov_lambda.ndim}d." + ) + + inverse = np.linalg.inv(matrix.T @ matrix + tikhonov_matrix) @ matrix.T + + return inverse + def calculate_VC( self, eq, @@ -325,6 +390,7 @@ def calculate_VC( starting_dI=None, min_starting_dI=50, verbose=False, + tikhonov_lambda=None, name=None, ): """ @@ -454,15 +520,18 @@ def calculate_VC( print("Inverting the shape matrix to get the virtual circuit matrix.") print(f"VC object stored under name: '{name}'.") + # vc matrix is pseudo inverse of shape matrix + vc_matrix = self.calculate_matrix_inverse( + shape_matrix, tikhonov_lambda=tikhonov_lambda + ) + # store the VC object dynamically store_VC = VirtualCircuit( name=name, eq=eq, profiles=profiles, shape_matrix=shape_matrix, - VCs_matrix=np.linalg.pinv( - shape_matrix - ), # "virtual circuits" are the pseudo-inverse of the shape matrix + VCs_matrix=vc_matrix, target_names=target_names, coils=coils, target_calculator=target_calculator, From c32ed3d06eb0404803868d23d0552b8c29af0040 Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Wed, 29 Apr 2026 10:19:52 +0100 Subject: [PATCH 09/11] add optional tikhonov regularisation to inverse calculation of VC matrix --- freegsnke/virtual_circuits.py | 75 +++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/freegsnke/virtual_circuits.py b/freegsnke/virtual_circuits.py index 73358b93..efccd951 100644 --- a/freegsnke/virtual_circuits.py +++ b/freegsnke/virtual_circuits.py @@ -314,6 +314,71 @@ def build_dIydI_j( return dtargets / final_dI + @staticmethod + def calculate_matrix_inverse( + matrix: np.ndarray, tikhonov_lambda: np.ndarray = None + ): + """ + Compute inverse of a generically non-square matrix + By default Moore Penrose inverse is used (np.pinv). + If Tikhonov_lambda is provided then Tikhonov regularisation is applied + and inv = [M^T M + diag(lambda)]^-1 M^T + + Inputs: + ------- + matrix : np.ndarray + matrix to be inverted + + tikhonov_lambda : np.ndarray, optional + 1d array of tikhonov coefficients, or 2d diagonal matrix of coefficients. + Must have size/shape consistent with matrix.shape[1]. + + + Returns: + -------- + inverse : np.ndarray + inverse of matrix + """ + matrix = np.asarray(matrix) # convert tensorflow to numpy. + if tikhonov_lambda is None: + # use regular moore-penrose pseudo inverse + print("Computing Moore-Penrose pseudoinverse") + inverse = np.linalg.pinv(matrix) + + else: + print("Computing Tikhonov regularised inverse") + tikhonov_lambda = np.asarray(tikhonov_lambda) + n_cols = matrix.shape[1] + + if tikhonov_lambda.ndim == 1: + if tikhonov_lambda.shape[0] != n_cols: + raise ValueError( + f"tikhonov_lambda length {tikhonov_lambda.shape[0]} " + f"must match matrix column count {n_cols}." + ) + tikhonov_matrix = np.diag(tikhonov_lambda) + + elif tikhonov_lambda.ndim == 2: + if tikhonov_lambda.shape != (n_cols, n_cols): + raise ValueError( + f"tikhonov_lambda shape {tikhonov_lambda.shape} " + f"must be ({n_cols}, {n_cols}) to match matrix column count." + ) + if not np.allclose(tikhonov_lambda, np.diag(np.diag(tikhonov_lambda))): + raise ValueError( + "tikhonov_lambda 2d array must be a diagonal matrix." + ) + tikhonov_matrix = tikhonov_lambda + + else: + raise ValueError( + f"tikhonov_lambda must be 1d or 2d, got {tikhonov_lambda.ndim}d." + ) + + inverse = np.linalg.inv(matrix.T @ matrix + tikhonov_matrix) @ matrix.T + + return inverse + def calculate_VC( self, eq, @@ -325,6 +390,7 @@ def calculate_VC( starting_dI=None, min_starting_dI=50, verbose=False, + tikhonov_lambda=None, name=None, ): """ @@ -454,15 +520,18 @@ def calculate_VC( print("Inverting the shape matrix to get the virtual circuit matrix.") print(f"VC object stored under name: '{name}'.") + # vc matrix is pseudo inverse of shape matrix + vc_matrix = self.calculate_matrix_inverse( + shape_matrix, tikhonov_lambda=tikhonov_lambda + ) + # store the VC object dynamically store_VC = VirtualCircuit( name=name, eq=eq, profiles=profiles, shape_matrix=shape_matrix, - VCs_matrix=np.linalg.pinv( - shape_matrix - ), # "virtual circuits" are the pseudo-inverse of the shape matrix + VCs_matrix=vc_matrix, target_names=target_names, coils=coils, target_calculator=target_calculator, From 193676d844233149580ee3a32844f100acef61db Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Tue, 28 Apr 2026 17:12:47 +0100 Subject: [PATCH 10/11] update to jacobian/vc storage --- freegsnke/control_loop/virtual_circuits_category.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freegsnke/control_loop/virtual_circuits_category.py b/freegsnke/control_loop/virtual_circuits_category.py index 5634164e..6e6c369e 100644 --- a/freegsnke/control_loop/virtual_circuits_category.py +++ b/freegsnke/control_loop/virtual_circuits_category.py @@ -286,8 +286,9 @@ def run_control( self.latest_vc = VC_shape_emu # store sensitivity matrix (Jacobian) - self.emulated_jacobian_list.append(self.vc_generator.jacobian_matrix) - self.emulated_vc_list.append(self.vc_generator.vc_matrix) + # self.emulated_jacobian_list.append(self.vc_generator.jacobian_matrix) + # self.emulated_vc_list.append(self.vc_generator.vc_matrix) + self.emulated_vc_list.append(self.latest_vc) self.emulated_vc_times.append(t) else: From 91ef1f34ecdc241d5c9a2b57e4f8f07f19b1b757 Mon Sep 17 00:00:00 2001 From: Alasdair Ross Date: Wed, 10 Jun 2026 15:16:27 +0100 Subject: [PATCH 11/11] update example09 virtual circuits to mention tikhonov regularisation --- .../example09 - virtual_circuits_MASTU.ipynb | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/examples/example09 - virtual_circuits_MASTU.ipynb b/examples/example09 - virtual_circuits_MASTU.ipynb index ab0918e0..460b6bb9 100644 --- a/examples/example09 - virtual_circuits_MASTU.ipynb +++ b/examples/example09 - virtual_circuits_MASTU.ipynb @@ -291,6 +291,10 @@ "\n", "$$ V = (S^T S)^{-1} S^T \\in \\Reals^{N_c \\times N_T}.$$\n", "\n", + "The matrix inversion above is the \"Moore-Penrose\" pseudo inverse which is used by default. We can also use a \"Tikhonov regularised\" inverse by providing an optional array or diagonal matrix of regularisation coeficients. This is computed as \n", + "\n", + "$$ V = (S^T S + \\Lambda ^T \\Lambda) ^{-1}S^T.$$\n", + "\n", "This matrix provides a mapping from requested shifts in the targets to the shifts in the coil currents required." ] }, @@ -328,6 +332,30 @@ " )" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# example of tikhonov inverse \n", + "lambda_matrix = 1e-12 * np.eye(len(coils)) #simple example proportional to the identity matrix\n", + "\n", + "VCs.calculate_VC(\n", + " eq=eq,\n", + " profiles=profiles,\n", + " coils=coils,\n", + " target_names=target_names,\n", + " target_calculator=plasma_descriptors,\n", + " starting_dI=None, \n", + " min_starting_dI=50,\n", + " tikhonov_lambda= lambda_matrix,\n", + " verbose=True,\n", + " \n", + " name=\"VC_for_lower_targets_with_tikhonov\", # name for the VC\n", + " )" + ] + }, { "cell_type": "code", "execution_count": null, @@ -651,20 +679,6 @@ "\n", "plt.tight_layout()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": {