Skip to content

Commit 2127824

Browse files
authored
Merge pull request #73 from Gromwud/main
Thesis update
2 parents b4e1d54 + 0b2a9f5 commit 2127824

44 files changed

Lines changed: 3739 additions & 274 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.git
2+
.gitignore
3+
.claude
4+
.idea
5+
projects/thesis/results
6+
projects/thesis/_test_archive
7+
__pycache__
8+
**/__pycache__
9+
*.pyc
10+
*.pdf
11+
*.rar
12+
Master_Thesis__Bavshin_.pdf
13+
moeadd.pdf

Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# python:3.10-slim has glibc, just enough to install the wheels.
2+
# numpy / scipy / sklearn / torch all ship manylinux wheels, so we
3+
# don't need a build toolchain at runtime.
4+
FROM python:3.10-slim
5+
6+
# libgomp1 is needed by numpy/sklearn/torch for OpenMP threading.
7+
RUN apt-get update && apt-get install -y --no-install-recommends \
8+
libgomp1 \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
WORKDIR /work
12+
13+
# Install Python deps first so the layer caches across source edits.
14+
COPY requirements.txt /work/requirements.txt
15+
RUN pip install --no-cache-dir --upgrade pip \
16+
&& pip install --no-cache-dir -r requirements.txt
17+
18+
# Copy the repo last so source edits don't bust the dep cache.
19+
COPY . /work
20+
21+
# Default to interactive shell; the actual sweep command is supplied
22+
# by docker-compose via the per-service ``command:`` field.
23+
ENTRYPOINT ["/bin/bash", "-l"]

docker-compose.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# 8 containers, one per ablation cell. All share the same image
2+
# and bind-mount projects/thesis/results/ so reps land on the host
3+
# filesystem and the aggregator can read them without entering a
4+
# container.
5+
#
6+
# Recommended invocation:
7+
# docker compose build
8+
# docker compose up -d # runs all 8 in background
9+
# docker compose logs -f cell-new # tail one cell's output
10+
# docker compose down # stops everything
11+
#
12+
# Resource notes:
13+
# - 16 host cores -> 2 threads/container is safe.
14+
# - 32 host cores -> bump OMP/MKL/OPENBLAS_NUM_THREADS to 4 for
15+
# ~half the wall-clock.
16+
# - The runner skips finished <cell>_rep<NN>.json files on
17+
# resume, so containers can be killed and relaunched freely.
18+
19+
services:
20+
cell-legacy: &cell-base
21+
build: .
22+
image: epde-thesis:latest
23+
command: ["scripts/run_cell.sh", "legacy"]
24+
volumes:
25+
- ./projects/thesis/results:/work/projects/thesis/results
26+
environment:
27+
OMP_NUM_THREADS: "2"
28+
MKL_NUM_THREADS: "2"
29+
OPENBLAS_NUM_THREADS: "2"
30+
restart: unless-stopped
31+
32+
cell-wape:
33+
<<: *cell-base
34+
command: ["scripts/run_cell.sh", "wape"]
35+
36+
cell-instab:
37+
<<: *cell-base
38+
command: ["scripts/run_cell.sh", "instab"]
39+
40+
cell-reg:
41+
<<: *cell-base
42+
command: ["scripts/run_cell.sh", "reg"]
43+
44+
cell-wape-instab:
45+
<<: *cell-base
46+
command: ["scripts/run_cell.sh", "wape_instab"]
47+
48+
cell-wape-reg:
49+
<<: *cell-base
50+
command: ["scripts/run_cell.sh", "wape_reg"]
51+
52+
cell-instab-reg:
53+
<<: *cell-base
54+
command: ["scripts/run_cell.sh", "instab_reg"]
55+
56+
cell-new:
57+
<<: *cell-base
58+
command: ["scripts/run_cell.sh", "new"]

epde/eq_mo_objectives.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,20 @@ def equation_complexity_by_terms(system, equation_key):
6161

6262

6363
def _complexity_single_eq(system, equation_key):
64+
# Index by ``weights_internal`` (always length ``len(structure)-1``,
65+
# one entry per non-target term in structure order) rather than
66+
# ``weights_final`` (zero-filtered to ``nnz+1`` by ``LASSOSparsity``
67+
# and ``VWSRSparsity``): structure-position indexing breaks against
68+
# ``weights_final`` whenever the sparsity step zeros more than one
69+
# weight.
70+
equation = system.vals[equation_key]
6471
eq_compl = 0
65-
for idx, term in enumerate(system.vals[equation_key].structure):
66-
if idx < system.vals[equation_key].target_idx:
67-
if not system.vals[equation_key].weights_final[idx] == 0:
72+
for idx, term in enumerate(equation.structure):
73+
if idx < equation.target_idx:
74+
if not equation.weights_internal[idx] == 0:
6875
eq_compl += complexity_deriv(term.structure)
69-
elif idx > system.vals[equation_key].target_idx:
70-
if not system.vals[equation_key].weights_final[idx-1] == 0:
76+
elif idx > equation.target_idx:
77+
if not equation.weights_internal[idx-1] == 0:
7178
eq_compl += complexity_deriv(term.structure)
7279
else:
7380
eq_compl += complexity_deriv(term.structure)

epde/interface/prepared_tokens.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,22 @@ def __init__(self, labels = ['t',], max_power: int = 1, dimensionality=1):
319319
320320
Args:
321321
dimensionality (`int`): optional, default - 1
322-
data dimension
322+
data dimension
323323
"""
324-
assert len(labels) == dimensionality + 1, 'Incorrect labels for grids.'
324+
# Two valid configurations:
325+
# 1. One label per axis (legacy): ``['x_0', ..., 'x_N']`` with
326+
# ``len == dimensionality + 1``. Each axis is its own token,
327+
# plus the ``dim`` param redundantly encodes the same axis.
328+
# 2. Single label (consolidated): ``['x']`` representing a single
329+
# family where the ``dim`` parameter is the ONLY axis
330+
# discriminator. Removes the redundant ``x_N`` prefix.
331+
# ``set_status(unique_token_type=True)`` already restricts to one
332+
# grid factor per term in either configuration, so behaviour is
333+
# identical -- the consolidation is a labelling cleanup.
334+
assert 1 <= len(labels) <= dimensionality + 1, (
335+
f'Grid labels must be either a single name or one per axis; '
336+
f'got {len(labels)} labels for dimensionality={dimensionality}'
337+
)
325338

326339
self._token_family = TokenFamily(token_type='grids')
327340
self._token_family.set_status(unique_specific_token=True, unique_token_type=True,

epde/operators/common/coeff_calculation.py

Lines changed: 88 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,91 +7,104 @@
77
"""
88

99
import numpy as np
10-
from sklearn.linear_model import LinearRegression, Ridge
10+
from sklearn.linear_model import LinearRegression
1111

1212
import epde.globals as global_var
1313
from epde.operators.utils.template import CompoundOperator
1414
from epde.structure.main_structures import Equation
1515

16+
17+
# Marker attribute set by ``LASSOSparsity.apply`` on the equation
18+
# instance to indicate that the legacy LASSO post-processing refit
19+
# (LinearRegression on the LASSO survivors, un-normalised features)
20+
# should run on this equation. ``VWSRSparsity`` does NOT set this
21+
# marker; its PhysicsInformedLasso output is already on the physical
22+
# scale and would be corrupted by the refit. Gating on the equation
23+
# (rather than on the operator) keeps the strategy wiring untouched.
24+
LEGACY_REFIT_MARKER = '_legacy_refit_pending'
25+
26+
1627
class LinRegBasedCoeffsEquation(CompoundOperator):
1728
'''
18-
19-
The operatror, dedicated to the calculation of the weights of the equation (for the free coefficient and
20-
each of its terms except the target one).
21-
22-
Attributes:
23-
_tags (`set`):
24-
g_fun_vals (`numpy.ndarray`):
25-
26-
Methods:
27-
apply(equation)
28-
Calculate the coefficients of the equation, using the linear regression. The result is stored in the
29-
equation.weights_final attribute
29+
Refit the LASSO survivors with ``LinearRegression`` on
30+
*un-normalised* features, replacing ``weights_final`` with
31+
physically-scaled coefficients.
32+
33+
Restores the legacy two-step pipeline:
34+
1. LASSOSparsity fits Lasso on min-max-normalised features and
35+
identifies the surviving (non-zero) terms.
36+
2. This operator re-fits those survivors with ordinary least
37+
squares on the un-normalised features to recover physical
38+
coefficient magnitudes (LASSO coefficients are biased by
39+
both L1 shrinkage and the upstream normalisation).
40+
41+
Gated by the per-equation marker ``LEGACY_REFIT_MARKER`` that
42+
``LASSOSparsity`` sets and ``VWSRSparsity`` leaves unset, so this
43+
operator can be wired into both pipelines without a strategy flag.
44+
45+
Output shape matches the upstream sparsity convention:
46+
``np.append(coef_, intercept)`` -- one entry per surviving non-zero
47+
feature plus a trailing intercept slot. Downstream consumers
48+
(``L2Fitness.apply``, ``L2LRFitness.apply``) need no change.
3049
'''
3150
key = 'LinRegCoeffCalc'
32-
51+
52+
@staticmethod
53+
def _legacy_evaluate_nonzero(objective: Equation):
54+
"""Build target + un-normalised feature matrix from the LASSO
55+
survivors, independent of ``Equation.evaluate``.
56+
57+
Mirrors the pre-aaea0f4 legacy feature builder: iterate the
58+
structure, skip the target, emit columns only for terms whose
59+
``weights_internal`` slot is non-zero. Returns
60+
``(target, features)`` with ``features=None`` when every
61+
non-target slot was filtered to zero.
62+
"""
63+
target = objective.structure[objective.target_idx].evaluate(False)
64+
feats = []
65+
for term_idx, term in enumerate(objective.structure):
66+
if term_idx == objective.target_idx:
67+
continue
68+
wi_pos = (term_idx if term_idx < objective.target_idx
69+
else term_idx - 1)
70+
if objective.weights_internal[wi_pos] != 0:
71+
feats.append(term.evaluate(False))
72+
if not feats:
73+
return target, None
74+
features = np.vstack(feats)
75+
if features.ndim == 1:
76+
features = np.expand_dims(features, 1).T
77+
features = np.transpose(features)
78+
return target, features
79+
3380
def apply(self, objective : Equation, arguments : dict = None):
81+
"""Refit LASSO survivors with un-normalised LinearRegression.
82+
83+
Skipped unless ``LASSOSparsity`` set ``LEGACY_REFIT_MARKER`` on
84+
the equation. The marker is cleared after the refit so a stale
85+
marker from a previous run doesn't double-trigger work.
3486
"""
35-
Calculate the coefficients of the equation, using the linear regression.The result is stored in the
36-
objective.weights_final attribute
37-
38-
Args:
39-
objective (`Equation`): the equation object, to that the fitness function is obtained.
40-
arguments (`dict`):
41-
42-
Returns:
43-
None
44-
"""
45-
# self_args, subop_args = self.parse_suboperator_args(arguments = arguments)
46-
47-
assert objective.weights_internal_evald, 'Trying to calculate final weights before evaluating intermeidate ones (no sparsity).'
48-
# target = objective.structure[objective.target_idx]
49-
#
50-
# target_vals = target.evaluate(False)
51-
# features_vals = []
52-
# nonzero_features_indexes = []
53-
# for i in range(len(objective.structure)):
54-
# if i == objective.target_idx:
55-
# continue
56-
# idx = i if i < objective.target_idx else i-1
57-
# if objective.weights_internal[idx] != 0:
58-
# features_vals.append(objective.structure[i].evaluate(False))
59-
# nonzero_features_indexes.append(idx)
60-
#
61-
# if len(features_vals) == 0:
62-
# objective.weights_final = np.zeros(len(objective.structure))
63-
# else:
64-
# features = features_vals[0]
65-
# if len(features_vals) > 1:
66-
# for i in range(1, len(features_vals)):
67-
# features = np.vstack([features, features_vals[i]])
68-
# features = np.vstack([features, np.ones(features_vals[0].shape)]) # Добавляем константную фичу
69-
# features = np.transpose(features)
70-
# estimator = LinearRegression(copy_X=True, fit_intercept=False, n_jobs=-1,
71-
# positive=False, tol=0.0001)
72-
# # estimator = LinearRegression(fit_intercept=False)
73-
# if features.ndim == 1:
74-
# features = features.reshape(-1, 1)
75-
# try:
76-
# self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func != 0]
77-
# except AttributeError:
78-
# self.g_fun_vals = None
79-
# estimator.fit(features, target_vals, sample_weight = self.g_fun_vals)
80-
#
81-
# valuable_weights = estimator.coef_
82-
# weights = np.zeros(len(objective.structure))
83-
# for weight_idx in range(len(weights)-1):
84-
# if weight_idx in nonzero_features_indexes:
85-
# weights[weight_idx] = valuable_weights[nonzero_features_indexes.index(weight_idx)]
86-
# weights[-1] = valuable_weights[-1]
87-
# objective.weights_final = weights
88-
# _, target, features = objective.evaluate(normalize=False, return_val=False)
89-
# self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func != 0]
90-
# estimator = LinearRegression(copy_X=True, fit_intercept=True, n_jobs=-1, positive=False, tol=0.0001)
91-
# estimator.fit(features, target, sample_weight=self.g_fun_vals)
92-
# valuable_weights = estimator.coef_
93-
# objective.weights_final = np.append(valuable_weights, estimator.intercept_)
94-
# objective.weights_final_evald = True
95-
87+
assert objective.weights_internal_evald, (
88+
'Trying to calculate final weights before evaluating '
89+
'intermediate ones (no sparsity).'
90+
)
91+
if not getattr(objective, LEGACY_REFIT_MARKER, False):
92+
return
93+
94+
target, features = self._legacy_evaluate_nonzero(objective)
95+
if features is None:
96+
# No non-zero terms to refit -- leave ``weights_final`` as
97+
# set by the upstream sparsity step (just the intercept).
98+
objective.weights_final_evald = True
99+
setattr(objective, LEGACY_REFIT_MARKER, False)
100+
return
101+
102+
self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func_mask]
103+
estimator = LinearRegression(copy_X=True, fit_intercept=True, n_jobs=-1)
104+
estimator.fit(features, target, sample_weight=self.g_fun_vals)
105+
objective.weights_final = np.append(estimator.coef_, estimator.intercept_)
106+
objective.weights_final_evald = True
107+
setattr(objective, LEGACY_REFIT_MARKER, False)
108+
96109
def use_default_tags(self):
97110
self._tags = {'coefficient calculation', 'gene level', 'no suboperators', 'inplace'}

0 commit comments

Comments
 (0)