Skip to content

Commit f7b2640

Browse files
Fix build breaks.
1 parent 8fbf21f commit f7b2640

7 files changed

Lines changed: 151 additions & 165 deletions

notebooks/change_point.ipynb

Lines changed: 13 additions & 13 deletions
Large diffs are not rendered by default.

scripts/analyze_piecewise_system_discovery.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def main(num_change_point: int = 2, is_initialize: bool = False) -> pd.DataFrame
5656
try:
5757
psd = PiecewiseSystemDiscovery(
5858
item.timecourse,
59-
num_change_point=num_change_point,
59+
max_change_point=num_change_point,
6060
).fit()
6161
info = psd.score()
6262
except Exception as exc:

src/change_point_detector.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,15 @@ def _get_adj_sum_sq(self, start: int, end: int) -> float:
6161

6262
return cumulative_ssq - (cumulative_sum**2 / length)
6363

64-
def _find_best_split(self, start: int, end: int) -> Tuple[int, float]:
64+
def _find_best_split(self, start: int, end: int, min_segment_length: int = 1) -> Tuple[int, float]:
6565
"""
6666
Find the optimal single split point for the range [start, end).
6767
6868
Returns:
6969
Tuple[int, float]: (best_split_idx, reduction_in_ASS)
7070
"""
7171
length = end - start
72-
if length <= 1:
72+
if length < 2 * min_segment_length:
7373
return -1, 0.0
7474

7575
# The sum of the total range for the reduction calculation
@@ -80,8 +80,8 @@ def _find_best_split(self, start: int, end: int) -> Tuple[int, float]:
8080
best_k = -1
8181

8282
# Split index k: data[start:k] and data[k:end]
83-
# k ranges from start + 1 to end - 1
84-
for k in range(start + 1, end):
83+
# k must be such that k - start >= min_segment_length and end - k >= min_segment_length
84+
for k in range(start + min_segment_length, end - min_segment_length + 1):
8585
# First partition: [start, k), length k - start
8686
sum_1 = self._get_sum(start, k)
8787
# Second partition: [k, end), length end - k
@@ -126,7 +126,7 @@ def fit(self, max_change_point: int, min_fractional_reduction: float,
126126
# largest reduction first so the fixed-K budget is spent optimally.
127127
# Each entry: (-reduction, start, end, k) — negated for min-heap ordering.
128128
heap: List[Tuple[float, int, int, int]] = []
129-
k_init, red_init = self._find_best_split(0, self.length)
129+
k_init, red_init = self._find_best_split(0, self.length, min_segment_length)
130130
if red_init > 0:
131131
heapq.heappush(heap, (-red_init, 0, self.length, k_init))
132132

@@ -135,8 +135,8 @@ def fit(self, max_change_point: int, min_fractional_reduction: float,
135135

136136
while heap and len(change_points) < max_change_point:
137137
neg_red, start, end, k = heapq.heappop(heap)
138-
if end - start <= min_segment_length:
139-
continue # Skip segments that are too short to split
138+
if end - start < 2 * min_segment_length:
139+
continue # Skip segments that are too short to split into two min_segment_length pieces
140140
reduction = -neg_red
141141

142142
if reduction <= min_fractional_reduction * a_total:
@@ -146,8 +146,8 @@ def fit(self, max_change_point: int, min_fractional_reduction: float,
146146
total_abs_reduction += reduction
147147

148148
for seg_start, seg_end in ((start, k), (k, end)):
149-
if seg_end - seg_start > 1:
150-
k_sub, red_sub = self._find_best_split(seg_start, seg_end)
149+
if seg_end - seg_start >= 2 * min_segment_length:
150+
k_sub, red_sub = self._find_best_split(seg_start, seg_end, min_segment_length)
151151
if red_sub > 0:
152152
heapq.heappush(heap, (-red_sub, seg_start, seg_end, k_sub))
153153

src/piecewise_system_discovery.py

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,31 @@ class PiecewiseSystemDiscovery(object):
2828
def __init__(
2929
self,
3030
timecourse: Timecourse,
31-
num_change_point: int = 2,
32-
min_normalized_reduction: float = 0.1,
31+
max_change_point: int = 2,
32+
min_fractional_reduction: float = 0.1,
3333
min_segment_length: int = 100,
3434
change_point_threshold: float = 0.1,
3535
predict_kernel_bandwidth: float = 0.5,
36-
**kwargs: Any,
36+
**sd_kwargs: Any,
3737
) -> None:
3838
"""_summary_
3939
4040
Args:
4141
timecourse (Timecourse): _description_
42-
num_change_point (int, optional): _description_. Defaults to 2.
43-
min_normalized_reduction (float, optional): _description_. Defaults to 0.1.
42+
max_change_point (int, optional): _description_. Defaults to 2.
43+
min_fractional_reduction (float, optional): _description_. Defaults to 0.1.
4444
min_segment_length (int, optional): _description_. Defaults to 100.
4545
change_point_threshold (float, optional): _description_. Defaults to 0.1.
46+
predict_kernel_bandwidth (float, optional): _description_. Defaults to 0.5.
47+
**kwargs: Arguments for SystemDiscovery constructor (e.g. fit_kernel_bandwidth, model_name).
4648
"""
4749
self.timecourse = timecourse
48-
self.num_change_point = num_change_point
49-
self.min_normalized_reduction = min_normalized_reduction
50+
self.max_change_point = max_change_point
51+
self.min_fractional_reduction = min_fractional_reduction
5052
self.min_segment_length = min_segment_length
5153
self.change_point_threshold = change_point_threshold
5254
self.predict_kernel_bandwidth = predict_kernel_bandwidth
53-
self._kwargs = kwargs
55+
self._sd_kwargs = sd_kwargs
5456

5557
self._segment_models: List[SystemDiscovery] = []
5658
self._segment_boundaries: List[Tuple[float, float]] = []
@@ -126,34 +128,21 @@ def _detectChangePoints(self, signal_arr: np.ndarray, num_point: int) -> List[in
126128
Returns a sorted (by time) list of accepted interior split indices.
127129
"""
128130
detector = ChangePointDetector(signal_arr)
129-
detector.fit(max_change_point=self.num_change_point,
130-
min_fractional_reduction=0.0,
131-
min_segment_length=2)
132-
if len(detector.subsequences) == 0:
133-
split_indices = []
134-
else:
135-
split_indices = [info.splice_start for info in detector.subsequences]
136-
split_indices.remove(0) # Remove the first index if it's 0, as we don't want to split at the very beginning
131+
# self.min_fraction_reduction gates the SSE reduction a split must achieve
132+
# (as a fraction of the total adjusted sum of squares), not the raw
133+
# signal value at the split point: the split index itself often falls
134+
# on a low-signal point right after a spike, since the detector
135+
# chooses the boundary that best separates two segments, not the
136+
# point with the largest signal value.
137+
detector.fit(max_change_point=self.max_change_point,
138+
min_fractional_reduction=self.min_fractional_reduction,
139+
min_segment_length=self.min_segment_length)
140+
141+
# Map signal index k to timecourse split index k + 1
142+
split_indices = [info.splice_start + 1 for info in detector.subsequences
143+
if info.splice_start > 0]
144+
137145
return sorted(split_indices)
138-
# candidate_index_arr = np.arange(1, num_point)
139-
# order_arr = np.argsort(-signal_arr, kind="stable")
140-
# accepted: List[int] = []
141-
# for rank in order_arr:
142-
# signal_value = signal_arr[rank]
143-
# if signal_value < self.change_point_threshold:
144-
# break
145-
# split_idx = int(candidate_index_arr[rank])
146-
# pos = bisect.bisect_left(accepted, split_idx)
147-
# left_bound = accepted[pos - 1] if pos > 0 else 0
148-
# right_bound = accepted[pos] if pos < len(accepted) else num_point
149-
# if (split_idx - left_bound) < self.min_segment_length:
150-
# continue
151-
# if (right_bound - split_idx) < self.min_segment_length:
152-
# continue
153-
# accepted.insert(pos, split_idx)
154-
# if len(accepted) == self.num_change_point:
155-
# break
156-
# return accepted
157146

158147
def fit(self) -> "PiecewiseSystemDiscovery":
159148
"""fit() steps 1-4: detect change points, fit per-segment models."""
@@ -174,7 +163,7 @@ def fit(self) -> "PiecewiseSystemDiscovery":
174163
end_time = time_arr[hi] if hi < num_point else time_arr[-1]
175164
self._segment_boundaries.append((float(time_arr[lo]), float(end_time)))
176165
self._segment_lengths.append(hi - lo)
177-
model = SystemDiscovery(segment_df, **self._kwargs).fit()
166+
model = SystemDiscovery(segment_df, **self._sd_kwargs).fit()
178167
self._segment_models.append(model)
179168

180169
self._is_fitted = True
@@ -201,7 +190,7 @@ def dynamicFit(self) -> "PiecewiseSystemDiscovery":
201190
jacobian_collection_arr = self.timecourse.jacobian_collection_arr
202191
num_point = raw_df.shape[0]
203192
time_arr = raw_df.index.to_numpy(dtype=float)
204-
num_seg = self.num_change_point + 1
193+
num_seg = self.max_change_point + 1
205194
mseg = self.min_segment_length
206195

207196
signal_arr = np.nan_to_num(
@@ -264,7 +253,7 @@ def dynamicFit(self) -> "PiecewiseSystemDiscovery":
264253
end_time = time_arr[hi] if hi < num_point else time_arr[-1]
265254
self._segment_boundaries.append((float(time_arr[lo]), float(end_time)))
266255
self._segment_lengths.append(hi - lo)
267-
model = SystemDiscovery(segment_df, **self._kwargs).fit()
256+
model = SystemDiscovery(segment_df, **self._sd_kwargs).fit()
268257
self._segment_models.append(model)
269258

270259
self._is_fitted = True
@@ -367,7 +356,7 @@ def plotPiecewise(self, num_true_point: int = 20, **kwargs: Any) -> PlotOptions:
367356
species_names = self._segment_models[0].species_names
368357
num_skip = max(1, len(time_arr) // num_true_point)
369358

370-
baseline = SystemDiscovery(timecourse_df, **self._kwargs).fit()
359+
baseline = SystemDiscovery(timecourse_df, **self._sd_kwargs).fit()
371360
try:
372361
baseline_pred_df = baseline.predict()
373362
except Exception:
@@ -405,7 +394,7 @@ def _draw(po: PlotOptions, pred_df: pd.DataFrame | None, title: str,
405394
po.apply()
406395

407396
_draw(PlotOptions(fig=fig, ax=ax_top, **kwargs), baseline_pred_df, "0 change points")
408-
_draw(plot_options, psd_pred_df, f"{self.num_change_point} change point(s)",
397+
_draw(plot_options, psd_pred_df, f"{self.max_change_point} change point(s)",
409398
vlines=change_point_times)
410399
fig.suptitle("Actual vs Predicted", fontsize=13, fontweight="bold")
411400
fig.tight_layout()

tests/test_analyze_piecewise_system_discovery.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ def test_num_change_point_passed_to_piecewise_system_discovery(self) -> None:
254254
received: list[int] = []
255255
items = [_makeIteratorItem("model_A")]
256256

257-
def fake_psd_init(timecourse, num_change_point=2, **kwargs):
258-
received.append(num_change_point)
257+
def fake_psd_init(timecourse, max_change_point=2, **kwargs):
258+
received.append(max_change_point)
259259
mock = MagicMock()
260260
mock.fit.return_value = mock
261261
mock.score.return_value = _makeScoreInfo()

0 commit comments

Comments
 (0)