Skip to content

Commit 823ea2c

Browse files
committed
style: resolve static analysis and formatting issues for CI
1 parent e6c591a commit 823ea2c

10 files changed

Lines changed: 340 additions & 81 deletions

File tree

.gemini/GEMINI.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# GitHub Interactions and CI Protocol
2+
3+
When interacting with Git and GitHub (committing, branching, updating code, and pushing), you must strictly follow the `git-commit-protocol` skill.
4+
5+
## Key Rules for GitHub Interactions
6+
1. **Follow Conventional Commits:** All your commit messages must follow the standard `<type>: <description>` format defined in the protocol.
7+
2. **Atomic Commits:** Make sure your commits contain single logical units of work.
8+
3. **Pre-Push CI Verification (CRITICAL):** Before executing `git push`, you must always inspect the local repository's CI workflow files (e.g. `.github/workflows/ci.yml`). You must identify the commands run by the CI (such as `ruff format --check .`, `ruff check .`, `pytest`, etc.) and **run them locally**. If any step fails, you must fix the code to resolve the linting or test failures before you are allowed to push to the remote repository.

scripts/evaluate_predictions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
)
2424

2525
# Configuration overrides for evaluation
26-
TRAIN_DIR = "data/Bundle1"
27-
EVAL_DIR = "data/Bundle2"
26+
TRAIN_DIR = "data/Bundle2"
27+
EVAL_DIR = "data/Bundle3"
2828
OUTPUT_DIR = "output"
2929

3030

scripts/experiment_moead.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def run_experiment():
121121
metric_vals = []
122122
for run in range(num_runs):
123123
moead = MOEADOptimizer()
124-
metrics, _ = moead.generate_pareto_front(
124+
metrics, _, _ = moead.generate_pareto_front(
125125
num_points=pop_size, generations=gens, verbose=False, **data_kwargs
126126
)
127127
obt_front = np.column_stack([metrics["Return"], metrics["Risk"]])
@@ -160,7 +160,7 @@ def run_experiment():
160160

161161
for run in range(num_runs):
162162
moead = MOEADOptimizer()
163-
metrics, _, history = moead.generate_pareto_front(
163+
metrics, _, _, history = moead.generate_pareto_front(
164164
num_points=pop_size,
165165
generations=gens,
166166
verbose=False,
@@ -187,12 +187,15 @@ def run_experiment():
187187

188188
# 5. Final Population Comparison (2D)
189189
print("Comparing Final Populations...")
190-
moead_metrics, moead_weights = moead.generate_pareto_front(
190+
moead_metrics, moead_weights_assets, weight_vectors = moead.generate_pareto_front(
191191
num_points=100, generations=200, verbose=False, **data_kwargs
192192
)
193193
plot_moead_2d_comparison(
194194
ref_metrics,
195195
moead_metrics,
196+
weight_vectors=weight_vectors,
197+
z_ideal=moead.z_ideal,
198+
z_nadir=moead.z_nadir,
196199
save_path=os.path.join(run_folder, "2d_comparison.png"),
197200
)
198201
print("Comparison plot saved.")
@@ -202,7 +205,7 @@ def run_experiment():
202205
num_points_2d = len(next(iter(moead_metrics.values())))
203206
for idx in range(num_points_2d):
204207
record = {name: values[idx] for name, values in moead_metrics.items()}
205-
for j, weight in enumerate(moead_weights[idx]):
208+
for j, weight in enumerate(moead_weights_assets[idx]):
206209
record[ASSET_NAMES[j]] = weight
207210
records_2d.append(record)
208211
df_pareto_2d = pd.DataFrame(records_2d)
@@ -219,7 +222,7 @@ def run_experiment():
219222
)
220223
moead_3d = MOEADOptimizer(problem=prob_3d)
221224

222-
metrics_3d, weights_3d = moead_3d.generate_pareto_front(
225+
metrics_3d, weights_3d, _ = moead_3d.generate_pareto_front(
223226
num_points=150, generations=300, verbose=False, **data_kwargs
224227
)
225228
plot_moead_3d_pareto(

scripts/experiment_repair_bias.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import numpy as np
2+
import matplotlib.pyplot as plt
3+
import os
4+
import sys
5+
6+
# Add the project root to sys.path so we can import src
7+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
8+
9+
from src.portfolio.moead import MOEADOptimizer
10+
11+
12+
def proj_simple_normalization(weights):
13+
"""
14+
Project weights by clipping negatives to 0 and normalizing the sum.
15+
"""
16+
weights = np.maximum(weights, 0)
17+
s = np.sum(weights)
18+
if s == 0:
19+
return np.ones(len(weights)) / len(weights)
20+
return weights / s
21+
22+
23+
def map_to_2d(weights):
24+
w2 = weights[:, 1]
25+
w3 = weights[:, 2]
26+
27+
x = w2 + 0.5 * w3
28+
y = (np.sqrt(3.0) / 2.0) * w3
29+
return x, y
30+
31+
32+
def plot_triangle(ax):
33+
triangle = np.array([[0, 0], [1, 0], [0.5, np.sqrt(3) / 2], [0, 0]])
34+
ax.plot(triangle[:, 0], triangle[:, 1], "k-", lw=1.5)
35+
ax.set_aspect("equal")
36+
ax.axis("off")
37+
38+
39+
def run_simulation():
40+
np.random.seed(42)
41+
N_points = 2000
42+
N_iterations = 200 # Increased to allow operators to reach stationary distribution
43+
44+
# 1. Start with a uniform distribution of points on the 3-simplex
45+
initial_points = np.random.dirichlet(np.ones(3), N_points)
46+
47+
points_euclidean = initial_points.copy()
48+
points_norm = initial_points.copy()
49+
50+
optimizer = MOEADOptimizer(problem=None)
51+
52+
# 2. Iterate
53+
for i in range(N_iterations):
54+
# Create random pairs for crossover
55+
indices = np.random.permutation(N_points)
56+
p1_idx = indices[: N_points // 2]
57+
p2_idx = indices[N_points // 2 :]
58+
59+
# We process Euclidean population
60+
offspring_euclidean = np.empty((N_points, 3))
61+
for j in range(len(p1_idx)):
62+
p1 = points_euclidean[p1_idx[j]]
63+
p2 = points_euclidean[p2_idx[j]]
64+
# Generate two children per pair to keep population size constant
65+
offspring_euclidean[2 * j] = optimizer._sbx_crossover(p1, p2)
66+
# Second child: technically MOEAD generates one randomly, we'll force the other to keep pops equal,
67+
# but simpler to just call it again
68+
offspring_euclidean[2 * j + 1] = optimizer._sbx_crossover(p1, p2)
69+
70+
for k in range(N_points):
71+
offspring_euclidean[k] = optimizer._polynomial_mutation(
72+
offspring_euclidean[k]
73+
)
74+
# Apply Euclidean projection from MOEAD
75+
points_euclidean[k] = optimizer._repair(offspring_euclidean[k])
76+
77+
# We process Normalization population
78+
offspring_norm = np.empty((N_points, 3))
79+
for j in range(len(p1_idx)):
80+
p1 = points_norm[p1_idx[j]]
81+
p2 = points_norm[p2_idx[j]]
82+
offspring_norm[2 * j] = optimizer._sbx_crossover(p1, p2)
83+
offspring_norm[2 * j + 1] = optimizer._sbx_crossover(p1, p2)
84+
85+
for k in range(N_points):
86+
offspring_norm[k] = optimizer._polynomial_mutation(offspring_norm[k])
87+
points_norm[k] = proj_simple_normalization(offspring_norm[k])
88+
89+
# 3. Visualization
90+
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
91+
92+
# Using hexbin to clearly show the density and fix the "no difference" plotting issue
93+
94+
ax = axes[0]
95+
plot_triangle(ax)
96+
x, y = map_to_2d(initial_points)
97+
hb = ax.hexbin(x, y, gridsize=30, cmap="viridis", mincnt=1)
98+
fig.colorbar(hb, ax=ax, label="Count")
99+
ax.set_title("Initial (Uniform)")
100+
101+
ax = axes[1]
102+
plot_triangle(ax)
103+
x, y = map_to_2d(points_euclidean)
104+
hb = ax.hexbin(x, y, gridsize=30, cmap="viridis", mincnt=1)
105+
fig.colorbar(hb, ax=ax, label="Count")
106+
ax.set_title(
107+
f"Euclidean Projection\n(After {N_iterations} iters with SBX & PolyMut)"
108+
)
109+
110+
ax = axes[2]
111+
plot_triangle(ax)
112+
x, y = map_to_2d(points_norm)
113+
hb = ax.hexbin(x, y, gridsize=30, cmap="viridis", mincnt=1)
114+
fig.colorbar(hb, ax=ax, label="Count")
115+
ax.set_title(
116+
f"Simple Normalization\n(After {N_iterations} iters with SBX & PolyMut)"
117+
)
118+
119+
plt.tight_layout()
120+
output_path = os.path.join("plots", "repair_bias_simulation.png")
121+
os.makedirs("plots", exist_ok=True)
122+
plt.savefig(output_path, dpi=150, bbox_inches="tight")
123+
print(f"Result saved to {output_path}")
124+
125+
126+
if __name__ == "__main__":
127+
run_simulation()

scripts/verify_variants.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import os
22
import sys
33
import numpy as np
4-
from datetime import datetime
54

65
# Add project root to sys.path
76
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
87

98
from src.portfolio import (
10-
calculate_expected_returns_and_cov,
119
MOEADOptimizer,
1210
MOEADDRAOptimizer,
1311
MOEADAWAOptimizer,
@@ -16,45 +14,46 @@
1614
RiskObjective,
1715
)
1816

17+
1918
def run_test():
2019
print("Starting MOEA/D Variants Verification Test...")
21-
20+
2221
# Mock data for testing
2322
num_assets = 5
2423
expected_returns = np.array([0.1, 0.15, 0.12, 0.08, 0.11])
2524
cov_matrix = np.eye(num_assets) * 0.05
2625
cov_matrix[0, 1] = cov_matrix[1, 0] = 0.01
27-
28-
data_kwargs = {
29-
"expected_returns": expected_returns,
30-
"cov_matrix": cov_matrix
31-
}
32-
26+
27+
data_kwargs = {"expected_returns": expected_returns, "cov_matrix": cov_matrix}
28+
3329
problem = PortfolioProblem([ReturnObjective(), RiskObjective()])
34-
30+
3531
optimizers = {
3632
"Standard MOEA/D": MOEADOptimizer(problem=problem),
3733
"MOEA/D-DRA": MOEADDRAOptimizer(problem=problem),
38-
"MOEA/D-AWA": MOEADAWAOptimizer(problem=problem)
34+
"MOEA/D-AWA": MOEADAWAOptimizer(problem=problem),
3935
}
40-
36+
4137
for name, opt in optimizers.items():
4238
print(f"\nRunning {name}...")
4339
try:
4440
metrics, weights, w_vectors = opt.generate_pareto_front(
45-
num_points=50,
46-
generations=100,
47-
verbose=False,
48-
**data_kwargs
41+
num_points=50, generations=100, verbose=False, **data_kwargs
4942
)
5043
print(f" {name} completed successfully.")
5144
print(f" Points found: {len(weights)}")
52-
print(f" Return range: [{np.min(metrics['Return']):.4f}, {np.max(metrics['Return']):.4f}]")
53-
print(f" Risk range: [{np.min(metrics['Risk']):.4f}, {np.max(metrics['Risk']):.4f}]")
45+
print(
46+
f" Return range: [{np.min(metrics['Return']):.4f}, {np.max(metrics['Return']):.4f}]"
47+
)
48+
print(
49+
f" Risk range: [{np.min(metrics['Risk']):.4f}, {np.max(metrics['Risk']):.4f}]"
50+
)
5451
except Exception as e:
5552
print(f" {name} FAILED with error: {e}")
5653
import traceback
54+
5755
traceback.print_exc()
5856

57+
5958
if __name__ == "__main__":
6059
run_test()

src/portfolio/moead.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ def get_pareto_parameters(self, num_points, **kwargs):
1818
# ── Public interface ──────────────────────────────────────────────────────
1919

2020
def generate_pareto_front(
21-
self, num_points=100, generations=100, T=10, nr=2, verbose=False, crossover_operator="sbx", **kwargs
21+
self,
22+
num_points=100,
23+
generations=100,
24+
T=10,
25+
nr=2,
26+
verbose=False,
27+
crossover_operator="sbx",
28+
**kwargs,
2229
):
2330
"""
2431
MOEA/D with Tchebycheff decomposition, stable normalisation, and
@@ -145,21 +152,27 @@ def normalise(f):
145152

146153
if crossover_operator == "sbx":
147154
# Simulated Binary Crossover (SBX)
148-
offspring = self._sbx_crossover(population[p1_idx], population[p2_idx])
155+
offspring = self._sbx_crossover(
156+
population[p1_idx], population[p2_idx]
157+
)
149158
# CRITICAL: We MUST repair immediately after SBX
150159
offspring = self._repair(offspring)
151-
160+
152161
# Polynomial Mutation
153162
offspring = self._polynomial_mutation(offspring)
154163
# CRITICAL: Repair again because polynomial mutation pushes bounds
155164
offspring = self._repair(offspring)
156-
165+
157166
elif crossover_operator == "simplex":
158167
# Linear mix crossing the full valid line segment on the simplex.
159168
# It natively avoids breaking the simplex geometry.
160-
offspring = self._simplex_crossover(population[p1_idx], population[p2_idx])
169+
offspring = self._simplex_crossover(
170+
population[p1_idx], population[p2_idx]
171+
)
161172
else:
162-
raise ValueError(f"Unknown crossover operator: {crossover_operator}")
173+
raise ValueError(
174+
f"Unknown crossover operator: {crossover_operator}"
175+
)
163176

164177
# Evaluate offspring.
165178
off_f = np.array(
@@ -199,5 +212,10 @@ def normalise(f):
199212
}
200213

201214
if kwargs.get("record_history", False):
202-
return pareto_metrics, np.array(population), weight_vectors, np.array(history)
215+
return (
216+
pareto_metrics,
217+
np.array(population),
218+
weight_vectors,
219+
np.array(history),
220+
)
203221
return pareto_metrics, np.array(population), weight_vectors

0 commit comments

Comments
 (0)