STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
helpers.py
Go to the documentation of this file.
1"""Helper functions for optimization layers.
2
3Contains:
4- Pressure curve generation and manipulation
5- Optimization variable conversion
6- Utility functions shared across layers
7"""
8
9from __future__ import annotations
10
11from typing import Dict, Any, List, Tuple
12import numpy as np
13
14
16 segments: List[Dict[str, Any]],
17 n_points: int = 200,
18) -> Tuple[np.ndarray, np.ndarray]:
19 """
20 Generate a pressure curve from a list of segments.
21
22 Each segment can be 'linear' or 'blowdown' with its own duration and pressures.
23
24 Args:
25 segments: List of segment dicts with keys:
26 - type: 'linear' or 'blowdown'
27 - duration: segment duration in seconds
28 - start_pressure_psi: pressure at start of segment
29 - end_pressure_psi: pressure at end of segment
30 - decay_tau: time constant for blowdown (only for blowdown type)
31 n_points: Total number of points in output array
32
33 Returns:
34 time_array: Array of time points [s]
35 pressure_array: Array of pressures [psi]
36 """
37 if not segments:
38 return np.array([0.0]), np.array([500.0])
39
40 # Calculate total duration
41 total_duration = sum(seg["duration"] for seg in segments)
42
43 # Create time array
44 time_array = np.linspace(0, total_duration, n_points)
45 pressure_array = np.zeros(n_points)
46
47 # Build pressure curve segment by segment
48 t_start = 0.0
49 for seg in segments:
50 seg_type = seg["type"]
51 seg_duration = seg["duration"]
52 P_start = seg["start_pressure_psi"]
53 P_end = seg["end_pressure_psi"]
54 t_end = t_start + seg_duration
55
56 # Find indices for this segment
57 mask = (time_array >= t_start) & (time_array <= t_end)
58 t_local = time_array[mask] - t_start
59
60 if seg_type == "linear":
61 # Linear interpolation
62 if seg_duration > 0:
63 pressure_array[mask] = P_start + (P_end - P_start) * t_local / seg_duration
64 else:
65 pressure_array[mask] = P_start
66 elif seg_type == "blowdown":
67 # Exponential decay: P(t) = P_end + (P_start - P_end) * exp(-t/tau)
68 tau = seg.get("decay_tau", seg_duration * 0.3)
69 if tau <= 0:
70 tau = seg_duration * 0.3 # Use 30% as default
71 pressure_array[mask] = P_end + (P_start - P_end) * np.exp(-t_local / tau)
72 else:
73 # Default to linear
74 if seg_duration > 0:
75 pressure_array[mask] = P_start + (P_end - P_start) * t_local / seg_duration
76 else:
77 pressure_array[mask] = P_start
78
79 t_start = t_end
80
81 return time_array, pressure_array
82
83
85 x_segments: np.ndarray,
86 n_segments: int,
87 base_pressure_psi: float,
88 target_burn_time: float,
89 use_initial_as_base: bool = False,
90) -> List[Dict[str, Any]]:
91 """
92 Convert optimizer variables to segment list.
93
94 For each segment, optimizer provides:
95 - type (0=linear, 1=blowdown) - rounded to int
96 - duration_ratio (0-1, fraction of total burn time)
97 - start_pressure_ratio (0.7-1.0 for regulation, 0.3-1.0 for blowdown, ratio of base pressure)
98 - end_pressure_ratio (0.7-1.0 for regulation, 0.3-1.0 for blowdown, ratio of base pressure)
99 - decay_tau_ratio (0-1, fraction of segment duration, only for blowdown)
100
101 Args:
102 x_segments: Array of optimizer variables for segments
103 n_segments: Number of segments (1-20)
104 base_pressure_psi: Base pressure [psi] - either max (blowdown) or initial (regulation)
105 target_burn_time: Total burn time [s]
106 use_initial_as_base: If True, base_pressure_psi is initial pressure (for regulation)
107 If False, base_pressure_psi is max pressure (for blowdown)
108
109 Returns:
110 List of segment dicts
111 """
112 segments = []
113 vars_per_segment = 5 # type, duration_ratio, start_ratio, end_ratio, tau_ratio
114
115 # Ensure n_segments doesn't exceed available array size
116 max_available_segments = len(x_segments) // vars_per_segment
117 n_segments = min(n_segments, max_available_segments)
118 if n_segments < 1:
119 n_segments = 1 # At least one segment
120
121 # Normalize durations so they sum to 1.0
122 duration_ratios = []
123
124 for i in range(n_segments):
125 idx_base = i * vars_per_segment
126 if idx_base + 4 >= len(x_segments):
127 break # Not enough variables for this segment
128 duration_ratio = float(np.clip(x_segments[idx_base + 1], 0.01, 1.0))
129 duration_ratios.append(duration_ratio)
130
131 # Normalize so sum = 1.0
132 total_ratio = sum(duration_ratios)
133 if total_ratio > 0:
134 duration_ratios = [dr / total_ratio for dr in duration_ratios]
135
136 # Build segments
137 for i in range(n_segments):
138 idx_base = i * vars_per_segment
139 if idx_base + 4 >= len(x_segments):
140 break # Not enough variables for this segment
141 seg_type_val = float(np.clip(x_segments[idx_base], 0.0, 1.0))
142 seg_type = "blowdown" if seg_type_val >= 0.5 else "linear"
143 duration = duration_ratios[i] * target_burn_time if i < len(duration_ratios) else target_burn_time / n_segments
144
145 # CRITICAL: For regulation, pressure ratios are relative to INITIAL pressure
146 # For regulation, we want start_ratio ≈ end_ratio ≈ 1.0 (flat profile)
147 # For blowdown, we allow end_ratio < start_ratio
148 if use_initial_as_base:
149 # Regulation mode: ratios relative to initial pressure (0.7-1.0)
150 start_ratio = float(np.clip(x_segments[idx_base + 2], 0.7, 1.0))
151 end_ratio_raw = float(np.clip(x_segments[idx_base + 3], 0.7, 1.0))
152 # For regulation, allow slight drop but prefer flat (end ≈ start)
153 # Don't enforce end <= start strictly - optimizer can explore
154 end_ratio = end_ratio_raw # Allow end to be slightly higher for flexibility
155 else:
156 # Blowdown mode: ratios relative to max pressure (0.1-1.0)
157 start_ratio = float(np.clip(x_segments[idx_base + 2], 0.1, 1.0))
158 end_ratio_raw = float(np.clip(x_segments[idx_base + 3], 0.1, 1.0))
159 # Ensure end <= start for blowdown (physically valid)
160 end_ratio = min(end_ratio_raw, start_ratio)
161
162 tau_ratio = float(np.clip(x_segments[idx_base + 4], 0.1, 1.0))
163
164 # Convert ratios to absolute pressures using base pressure
165 start_pressure_psi = start_ratio * base_pressure_psi
166 end_pressure_psi = end_ratio * base_pressure_psi
167
168 seg = {
169 "type": seg_type,
170 "duration": duration,
171 "start_pressure_psi": start_pressure_psi,
172 "end_pressure_psi": end_pressure_psi,
173 }
174
175 if seg_type == "blowdown":
176 seg["decay_tau"] = duration * tau_ratio
177
178 segments.append(seg)
179
180 return segments
181
182
184 segments: List[Dict[str, Any]],
185 max_pressure_psi: float,
186 target_burn_time: float,
187) -> np.ndarray:
188 """
189 Convert segment list to optimizer variables.
190
191 Inverse of segments_from_optimizer_vars.
192 """
193 vars_per_segment = 5
194 n_segments = len(segments)
195 x = np.zeros(n_segments * vars_per_segment)
196
197 total_duration = sum(seg["duration"] for seg in segments)
198
199 for i, seg in enumerate(segments):
200 idx_base = i * vars_per_segment
201 x[idx_base] = 1.0 if seg["type"] == "blowdown" else 0.0
202 x[idx_base + 1] = seg["duration"] / total_duration if total_duration > 0 else 1.0 / n_segments
203 x[idx_base + 2] = seg["start_pressure_psi"] / max_pressure_psi
204 x[idx_base + 3] = seg["end_pressure_psi"] / max_pressure_psi
205 x[idx_base + 4] = seg.get("decay_tau", seg["duration"] * 0.3) / seg["duration"] if seg["duration"] > 0 else 0.3
206
207 return x
208
#define min(a, b)
Definition Arduino.h:42
np.ndarray optimizer_vars_from_segments(List[Dict[str, Any]] segments, float max_pressure_psi, float target_burn_time)
Definition helpers.py:187
List[Dict[str, Any]] segments_from_optimizer_vars(np.ndarray x_segments, int n_segments, float base_pressure_psi, float target_burn_time, bool use_initial_as_base=False)
Definition helpers.py:90
Tuple[np.ndarray, np.ndarray] generate_segmented_pressure_curve(List[Dict[str, Any]] segments, int n_points=200)
Definition helpers.py:18