From d173989b21cc7b8b2cdbb7a583c772bb3914eab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rian=20Rey?= Date: Wed, 20 May 2026 22:46:15 +0200 Subject: [PATCH 1/5] refactor(trajectories): Rename QuadraticForm to QuadraticFunction Co-Authored-By: Claude Sonnet 4.6 --- tests/trajectories/_constants.py | 12 ++++++------ tests/trajectories/_objectives.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/trajectories/_constants.py b/tests/trajectories/_constants.py index b916b081..40a70b31 100644 --- a/tests/trajectories/_constants.py +++ b/tests/trajectories/_constants.py @@ -18,11 +18,11 @@ UPGrad, ) from trajectories._objectives import ( - ConvexQuadraticForm, + ConvexQuadraticFunction, ElementWiseQuadratic, - HomogenousQuadraticForm, + HomogenousQuadraticFunction, Multinorm, - QuadraticForm, + QuadraticFunction, ) AGGREGATORS = { @@ -108,7 +108,7 @@ OBJECTIVES = { "EWQ": ElementWiseQuadratic(2), - "CQF": ConvexQuadraticForm( + "CQF": ConvexQuadraticFunction( Bs=[ torch.tensor([[cos(THETA), -sin(THETA)], [sin(THETA), cos(THETA)]]) @ torch.diag(torch.tensor([1.0, 0.1])), @@ -117,11 +117,11 @@ ], us=[torch.tensor([1.0, 0.0]), torch.tensor([-1.0, 0.0])], ), - "CQF2": QuadraticForm( + "CQF2": QuadraticFunction( As=[torch.tensor([[1.0, 0.2], [0.2, 0.05]]), torch.tensor([[3.0, -0.6], [-0.6, 0.2]])], us=[torch.tensor([1.0, 0.0]), torch.tensor([-1.0, 0.0])], ), - "HQF": HomogenousQuadraticForm( + "HQF": HomogenousQuadraticFunction( A=torch.tensor([[2.0, -1.0], [-1.0, 2.0]]), scales=torch.tensor([1.0, 10.0]), us=[torch.tensor([1.0, 0.0]), torch.tensor([-10.0, 0.0])], diff --git a/tests/trajectories/_objectives.py b/tests/trajectories/_objectives.py index e0c7de61..3789c5ce 100644 --- a/tests/trajectories/_objectives.py +++ b/tests/trajectories/_objectives.py @@ -45,7 +45,7 @@ def sps_mapping(self) -> "WithSPSMappingMixin.SPSMapping": pass -class QuadraticForm(Objective, WithSPSMappingMixin): +class QuadraticFunction(Objective, WithSPSMappingMixin): def __init__(self, As: list[Tensor], us: list[Tensor]) -> None: if len(As) != len(us): raise ValueError("As and us must have the same length.") @@ -86,11 +86,11 @@ def __call__(self, w: Tensor) -> Tensor: return torch.linalg.lstsq(G, b, driver="gelsd").solution @property - def sps_mapping(self) -> "QuadraticForm.SPSMapping": + def sps_mapping(self) -> "QuadraticFunction.SPSMapping": return self.SPSMapping(self.As, self.us) -class HomogenousQuadraticForm(QuadraticForm): +class HomogenousQuadraticFunction(QuadraticFunction): def __init__(self, A: Tensor, scales: Tensor, us: list[Tensor]) -> None: self.A = A self.scales = scales @@ -101,7 +101,7 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}(A={self.A}, scales={self.scales}, us={self.us})" -class ConvexQuadraticForm(QuadraticForm): +class ConvexQuadraticFunction(QuadraticFunction): def __init__(self, Bs: list[Tensor], us: list[Tensor]) -> None: self.Bs = Bs super().__init__(As=[B @ B.T for B in self.Bs], us=us) From cf88e3e7ce774a8d7d4a85743c6755e5b1f2084b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rian=20Rey?= Date: Wed, 20 May 2026 22:52:23 +0200 Subject: [PATCH 2/5] refactor(trajectories): Remove CQF, MN2, MN20 objectives and nashmtl20 aggregator Co-Authored-By: Claude Sonnet 4.6 --- CONTRIBUTING.md | 4 +- tests/trajectories/_constants.py | 49 ----------------------- tests/trajectories/_objectives.py | 37 ----------------- tests/trajectories/optimize.py | 2 +- tests/trajectories/plot_distance_to_pf.py | 2 +- tests/trajectories/plot_params.py | 2 +- tests/trajectories/plot_values.py | 2 +- 7 files changed, 6 insertions(+), 92 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4414f07e..d0c17c3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -234,9 +234,9 @@ The `tests/trajectories/` directory contains scripts to generate and visualize o trajectories using various aggregators on simple multi-objective problems. They require the `plot` dependency group. -Available objective keys: `EWQ`, `CQF`, `CQF2`, `HQF`, `MN2`, `MN20`. +Available objective keys: `EWQ`, `CQF2`, `HQF`. -Available aggregator keys: `upgrad`, `mgda`, `cagrad`, `nashmtl`, `nashmtl20`, `graddrop`, +Available aggregator keys: `upgrad`, `mgda`, `cagrad`, `nashmtl`, `graddrop`, `imtl_g`, `aligned_mtl`, `dualproj`, `pcgrad`, `random`, `mean`. **Step 1 — Optimize:** run the optimization for an objective and a selection of aggregators: diff --git a/tests/trajectories/_constants.py b/tests/trajectories/_constants.py index 40a70b31..e8c832d9 100644 --- a/tests/trajectories/_constants.py +++ b/tests/trajectories/_constants.py @@ -1,6 +1,3 @@ -from math import cos, sin - -import numpy as np import torch from torchjd._linalg import QuadprogProjector @@ -18,10 +15,8 @@ UPGrad, ) from trajectories._objectives import ( - ConvexQuadraticFunction, ElementWiseQuadratic, HomogenousQuadraticFunction, - Multinorm, QuadraticFunction, ) @@ -30,7 +25,6 @@ "mgda": MGDA(), "cagrad": CAGrad(c=0.5), "nashmtl": NashMTL(n_tasks=2, optim_niter=1), - "nashmtl20": NashMTL(n_tasks=20, optim_niter=1), "graddrop": GradDrop(), "imtl_g": IMTLG(), "aligned_mtl": AlignedMTL(), @@ -44,7 +38,6 @@ "mgda": 2.0, "cagrad": 1.0, "nashmtl": 2.0, - "nashmtl20": 2.0, "graddrop": 0.5, "imtl_g": 1.0, "aligned_mtl": 4.0, @@ -60,7 +53,6 @@ "nashmtl": 20.0, "imtl_g": 2.0, }, - "CQF": {"nashmtl": 0.5}, "CQF2": {"nashmtl": 0.5}, } AGGREGATOR_ORDER = { @@ -68,7 +60,6 @@ "mgda": 1, "cagrad": 5, "nashmtl": 7, - "nashmtl20": 7, "graddrop": 3, "imtl_g": 4, "aligned_mtl": 8, @@ -82,7 +73,6 @@ "mgda": r"$\mathcal A_{\mathrm{MGDA}}$", "cagrad": r"$\mathcal A_{\mathrm{CAGrad}}$", "nashmtl": r"$\mathcal A_{\mathrm{Nash-MTL}}$", - "nashmtl20": r"$\mathcal A_{\mathrm{Nash-MTL}}$", "graddrop": r"$\mathcal A_{\mathrm{GradDrop}}$", "imtl_g": r"$\mathcal A_{\mathrm{IMTL-G}}$", "aligned_mtl": r"$\mathcal A_{\mathrm{Aligned-MTL}}$", @@ -94,29 +84,14 @@ # Sometimes we need to override the xlim and ylim of the value plot to zoom enough PLOT_VALUES_LIMS = { - "CQF": { - "xlim": (-0.125, 2.625), - "ylim": (-0.425, 8.925), - }, "CQF2": { "xlim": (-0.125, 2.625), "ylim": (-0.425, 8.925), }, } -THETA = np.pi / 16 - OBJECTIVES = { "EWQ": ElementWiseQuadratic(2), - "CQF": ConvexQuadraticFunction( - Bs=[ - torch.tensor([[cos(THETA), -sin(THETA)], [sin(THETA), cos(THETA)]]) - @ torch.diag(torch.tensor([1.0, 0.1])), - torch.tensor([[cos(THETA), sin(THETA)], [-sin(THETA), cos(THETA)]]) - @ torch.diag(torch.tensor([torch.sqrt(torch.tensor(3.0)), 0.1])), - ], - us=[torch.tensor([1.0, 0.0]), torch.tensor([-1.0, 0.0])], - ), "CQF2": QuadraticFunction( As=[torch.tensor([[1.0, 0.2], [0.2, 0.05]]), torch.tensor([[3.0, -0.6], [-0.6, 0.2]])], us=[torch.tensor([1.0, 0.0]), torch.tensor([-1.0, 0.0])], @@ -126,16 +101,11 @@ scales=torch.tensor([1.0, 10.0]), us=[torch.tensor([1.0, 0.0]), torch.tensor([-10.0, 0.0])], ), - "MN2": Multinorm(torch.tensor([1.0, 10.0])), - "MN20": Multinorm(torch.arange(1, 21)), } BASE_LEARNING_RATES = { "EWQ": 0.075, - "CQF": 0.125, "CQF2": 0.125, "HQF": 0.005, - "MN2": 0.02, - "MN20": 0.005, } INITIAL_POINTS = { "EWQ": [ @@ -145,12 +115,6 @@ [-3.0, 4.0], [-3.5, -0.75], ], - "CQF": [ - [0.5, 0.5], - [-1.0, 7.0], - [0.0, 0.0], - [1.0, 6.0], - ], "CQF2": [ [0.5, 0.5], [-0.3, 7.0], @@ -162,22 +126,9 @@ [1.5, 2.0], [2.5, 5.5], ], - "MN2": [ - [0.0, 0.0], - [-5.0, 5.0], - [10.0, 5.0], - [10.0, 0.0], - [20.0, 0.0], - ], - "MN20": [ - [0.0] * 20, - ], } N_ITERS = { "EWQ": 50, - "CQF": 200, "CQF2": 200, "HQF": 100, - "MN2": 50, - "MN20": 500, } diff --git a/tests/trajectories/_objectives.py b/tests/trajectories/_objectives.py index 3789c5ce..ae47f962 100644 --- a/tests/trajectories/_objectives.py +++ b/tests/trajectories/_objectives.py @@ -101,15 +101,6 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}(A={self.A}, scales={self.scales}, us={self.us})" -class ConvexQuadraticFunction(QuadraticFunction): - def __init__(self, Bs: list[Tensor], us: list[Tensor]) -> None: - self.Bs = Bs - super().__init__(As=[B @ B.T for B in self.Bs], us=us) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(Bs={self.Bs}, us={self.us})" - - class ElementWiseQuadratic(Objective, WithSPSMappingMixin): def __init__(self, n_dim: int) -> None: super().__init__(n_params=n_dim, n_values=n_dim) @@ -132,31 +123,3 @@ def __call__(self, w: Tensor) -> Tensor: # noqa: ARG002 @property def sps_mapping(self) -> "ElementWiseQuadratic.SPSMapping": return self.SPSMapping(self.n_values) - - -class Multinorm(Objective, WithSPSMappingMixin): - def __init__(self, a: Tensor) -> None: - n = len(a) - super().__init__(n_params=n, n_values=n) - self.a = a - - def __call__(self, x: Tensor) -> Tensor: - if len(x) != self.n_values: - raise ValueError("x must have the same length as the number of values.") - - # f_i(x) = a_i * || x - a_i * e_i ||² - return self.a * torch.norm(x.expand(len(x), len(x)) - torch.diag(self.a), dim=1) ** 2 - - def jacobian(self, x: Tensor) -> Tensor: - return self.a * 2 * (x.expand(len(x), len(x)) - torch.diag(self.a)) - - class SPSMapping(WithSPSMappingMixin.SPSMapping): - def __init__(self, a: Tensor) -> None: - self.a = a - - def __call__(self, w: Tensor) -> Tensor: - return w * self.a - - @property - def sps_mapping(self) -> "Multinorm.SPSMapping": - return self.SPSMapping(self.a) diff --git a/tests/trajectories/optimize.py b/tests/trajectories/optimize.py index 8f0b9f34..a0fb39a0 100644 --- a/tests/trajectories/optimize.py +++ b/tests/trajectories/optimize.py @@ -6,7 +6,7 @@ uv run python tests/trajectories/optimize.py ... Arguments: - The key of the objective function (e.g., EWQ, CQF, HQF, MN2, MN20). + The key of the objective function (e.g., EWQ, CQF2, HQF). ... The keys of the aggregators to use (e.g., upgrad, mean, mgda). """ diff --git a/tests/trajectories/plot_distance_to_pf.py b/tests/trajectories/plot_distance_to_pf.py index 01c060ec..2ed07481 100644 --- a/tests/trajectories/plot_distance_to_pf.py +++ b/tests/trajectories/plot_distance_to_pf.py @@ -5,7 +5,7 @@ uv run python tests/trajectories/plot_distance_to_pf.py Arguments: - The key of the objective function (e.g., EWQ, CQF, HQF, MN2, MN20). + The key of the objective function (e.g., EWQ, CQF2, HQF). """ import argparse diff --git a/tests/trajectories/plot_params.py b/tests/trajectories/plot_params.py index 190d8dbe..06d96100 100644 --- a/tests/trajectories/plot_params.py +++ b/tests/trajectories/plot_params.py @@ -5,7 +5,7 @@ uv run python tests/trajectories/plot_params.py Arguments: - The key of the objective function (e.g., EWQ, CQF, HQF, MN2, MN20). + The key of the objective function (e.g., EWQ, CQF2, HQF). """ import argparse diff --git a/tests/trajectories/plot_values.py b/tests/trajectories/plot_values.py index 22b5f8a3..0e4379bd 100644 --- a/tests/trajectories/plot_values.py +++ b/tests/trajectories/plot_values.py @@ -5,7 +5,7 @@ uv run python tests/trajectories/plot_values.py Arguments: - The key of the objective function (e.g., EWQ, CQF, HQF, MN2, MN20). + The key of the objective function (e.g., EWQ, CQF2, HQF). """ import argparse From cd5f3023f80051603a280ac0fb859e6d63f0d539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rian=20Rey?= Date: Wed, 20 May 2026 22:53:43 +0200 Subject: [PATCH 3/5] refactor(trajectories): Rename CQF2 to CQF Co-Authored-By: Claude Sonnet 4.6 --- CONTRIBUTING.md | 2 +- tests/trajectories/_constants.py | 12 ++++++------ tests/trajectories/optimize.py | 2 +- tests/trajectories/plot_distance_to_pf.py | 2 +- tests/trajectories/plot_params.py | 2 +- tests/trajectories/plot_values.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0c17c3c..23bc1faa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -234,7 +234,7 @@ The `tests/trajectories/` directory contains scripts to generate and visualize o trajectories using various aggregators on simple multi-objective problems. They require the `plot` dependency group. -Available objective keys: `EWQ`, `CQF2`, `HQF`. +Available objective keys: `EWQ`, `CQF`, `HQF`. Available aggregator keys: `upgrad`, `mgda`, `cagrad`, `nashmtl`, `graddrop`, `imtl_g`, `aligned_mtl`, `dualproj`, `pcgrad`, `random`, `mean`. diff --git a/tests/trajectories/_constants.py b/tests/trajectories/_constants.py index e8c832d9..d4c25e9d 100644 --- a/tests/trajectories/_constants.py +++ b/tests/trajectories/_constants.py @@ -53,7 +53,7 @@ "nashmtl": 20.0, "imtl_g": 2.0, }, - "CQF2": {"nashmtl": 0.5}, + "CQF": {"nashmtl": 0.5}, } AGGREGATOR_ORDER = { "upgrad": 9, @@ -84,7 +84,7 @@ # Sometimes we need to override the xlim and ylim of the value plot to zoom enough PLOT_VALUES_LIMS = { - "CQF2": { + "CQF": { "xlim": (-0.125, 2.625), "ylim": (-0.425, 8.925), }, @@ -92,7 +92,7 @@ OBJECTIVES = { "EWQ": ElementWiseQuadratic(2), - "CQF2": QuadraticFunction( + "CQF": QuadraticFunction( As=[torch.tensor([[1.0, 0.2], [0.2, 0.05]]), torch.tensor([[3.0, -0.6], [-0.6, 0.2]])], us=[torch.tensor([1.0, 0.0]), torch.tensor([-1.0, 0.0])], ), @@ -104,7 +104,7 @@ } BASE_LEARNING_RATES = { "EWQ": 0.075, - "CQF2": 0.125, + "CQF": 0.125, "HQF": 0.005, } INITIAL_POINTS = { @@ -115,7 +115,7 @@ [-3.0, 4.0], [-3.5, -0.75], ], - "CQF2": [ + "CQF": [ [0.5, 0.5], [-0.3, 7.0], [0.0, 0.0], @@ -129,6 +129,6 @@ } N_ITERS = { "EWQ": 50, - "CQF2": 200, + "CQF": 200, "HQF": 100, } diff --git a/tests/trajectories/optimize.py b/tests/trajectories/optimize.py index a0fb39a0..9bd1b35c 100644 --- a/tests/trajectories/optimize.py +++ b/tests/trajectories/optimize.py @@ -6,7 +6,7 @@ uv run python tests/trajectories/optimize.py ... Arguments: - The key of the objective function (e.g., EWQ, CQF2, HQF). + The key of the objective function (e.g., EWQ, CQF, HQF). ... The keys of the aggregators to use (e.g., upgrad, mean, mgda). """ diff --git a/tests/trajectories/plot_distance_to_pf.py b/tests/trajectories/plot_distance_to_pf.py index 2ed07481..de5074f0 100644 --- a/tests/trajectories/plot_distance_to_pf.py +++ b/tests/trajectories/plot_distance_to_pf.py @@ -5,7 +5,7 @@ uv run python tests/trajectories/plot_distance_to_pf.py Arguments: - The key of the objective function (e.g., EWQ, CQF2, HQF). + The key of the objective function (e.g., EWQ, CQF, HQF). """ import argparse diff --git a/tests/trajectories/plot_params.py b/tests/trajectories/plot_params.py index 06d96100..d726eaa9 100644 --- a/tests/trajectories/plot_params.py +++ b/tests/trajectories/plot_params.py @@ -5,7 +5,7 @@ uv run python tests/trajectories/plot_params.py Arguments: - The key of the objective function (e.g., EWQ, CQF2, HQF). + The key of the objective function (e.g., EWQ, CQF, HQF). """ import argparse diff --git a/tests/trajectories/plot_values.py b/tests/trajectories/plot_values.py index 0e4379bd..08c33968 100644 --- a/tests/trajectories/plot_values.py +++ b/tests/trajectories/plot_values.py @@ -5,7 +5,7 @@ uv run python tests/trajectories/plot_values.py Arguments: - The key of the objective function (e.g., EWQ, CQF2, HQF). + The key of the objective function (e.g., EWQ, CQF, HQF). """ import argparse From 37c00f7696dcd3258bc37d321e644ae199486e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rian=20Rey?= Date: Wed, 20 May 2026 22:56:07 +0200 Subject: [PATCH 4/5] Add usage example to run all trajectories of the paper at once --- CONTRIBUTING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23bc1faa..7e1fb081 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -253,6 +253,23 @@ uv run python tests/trajectories/plot_values.py EWQ uv run python tests/trajectories/plot_distance_to_pf.py EWQ ``` +To run everything: +```bash +export MPLBACKEND=Agg +uv run python tests/trajectories/optimize.py EWQ upgrad mean mgda cagrad dualproj graddrop imtl_g aligned_mtl nashmtl random +uv run python tests/trajectories/plot_params.py EWQ +uv run python tests/trajectories/plot_values.py EWQ +uv run python tests/trajectories/plot_distance_to_pf.py EWQ +uv run python tests/trajectories/optimize.py CQF upgrad mean mgda cagrad dualproj graddrop imtl_g aligned_mtl nashmtl random +uv run python tests/trajectories/plot_params.py CQF +uv run python tests/trajectories/plot_values.py CQF +uv run python tests/trajectories/plot_distance_to_pf.py CQF +uv run python tests/trajectories/optimize.py HQF upgrad mean mgda cagrad dualproj graddrop imtl_g aligned_mtl nashmtl random +uv run python tests/trajectories/plot_params.py HQF +uv run python tests/trajectories/plot_values.py HQF +uv run python tests/trajectories/plot_distance_to_pf.py HQF +``` + Replace `EWQ` with any other objective key. The three plot scripts produce PDFs saved to `tests/trajectories/results//`. From c327a0eed95e4f7a5b947bc45b8e48463f86d7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Val=C3=A9rian=20Rey?= Date: Wed, 20 May 2026 22:57:24 +0200 Subject: [PATCH 5/5] Fixup --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e1fb081..a6a29c35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -270,8 +270,7 @@ uv run python tests/trajectories/plot_values.py HQF uv run python tests/trajectories/plot_distance_to_pf.py HQF ``` -Replace `EWQ` with any other objective key. The three plot scripts produce PDFs saved to -`tests/trajectories/results//`. +The three plot scripts produce PDFs saved to `tests/trajectories/results//`. > [!NOTE] > The plot scripts require a LaTeX installation for rendering: