openscvx 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openscvx might be problematic. Click here for more details.

openscvx/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.1'
21
- __version_tuple__ = version_tuple = (0, 2, 1)
20
+ __version__ = version = '0.2.2'
21
+ __version_tuple__ = version_tuple = (0, 2, 2)
File without changes
@@ -0,0 +1,119 @@
1
+ import numpy as np
2
+ from openscvx.backend.variable import Variable
3
+
4
+ class Control(Variable):
5
+ """A class representing the control variables in an optimal control problem.
6
+
7
+ The Control class extends Variable to handle control-specific properties and supports
8
+ both true and augmented control dimensions. It provides methods for appending new control
9
+ variables and accessing subsets of the control vector.
10
+
11
+ Attributes:
12
+ name (str): Name of the control variable.
13
+ shape (tuple): Shape of the control variable array.
14
+ min (np.ndarray): Minimum bounds for the control variables. Shape: (n_controls,).
15
+ max (np.ndarray): Maximum bounds for the control variables. Shape: (n_controls,).
16
+ guess (np.ndarray): Used to initialize SCP and contains the current SCP solution for the control trajectory. Shape: (n_nodes, n_controls).
17
+ _true_dim (int): True dimensionality of the control variables.
18
+ _true_slice (slice): Slice for accessing true control variables.
19
+ _augmented_slice (slice): Slice for accessing augmented control variables.
20
+
21
+ Notes:
22
+ Attributes prefixed with underscore (_) are for internal use only and should not be accessed directly.
23
+
24
+ Example:
25
+ ```python
26
+ control = Control("thrust", (3,))
27
+ control.min = [-1, -1, 0]
28
+ control.max = [1, 1, 10]
29
+ control.guess = np.repeat([[0, 0, 10]], 5, axis=0)
30
+ ```
31
+ """
32
+
33
+ def __init__(self, name, shape):
34
+ """Initialize a Control object.
35
+
36
+ Args:
37
+ name (str): Name identifier for the control variable
38
+ shape (tuple): Shape of the control vector
39
+ """
40
+ super().__init__(name, shape)
41
+ self._true_dim = shape[0]
42
+ self._update_slices()
43
+
44
+ def _update_slices(self):
45
+ """Update the slice objects for true and augmented controls."""
46
+ self._true_slice = slice(0, self._true_dim)
47
+ self._augmented_slice = slice(self._true_dim, self.shape[0])
48
+
49
+ def append(self, other=None, *, min=-np.inf, max=np.inf, guess=0.0, augmented=False):
50
+ """Append another control or create a new control variable.
51
+
52
+ Args:
53
+ other (Control, optional): Another Control object to append
54
+ min (float, optional): Minimum bound for new control. Defaults to -np.inf
55
+ max (float, optional): Maximum bound for new control. Defaults to np.inf
56
+ guess (float, optional): Initial guess for new control. Defaults to 0.0
57
+ augmented (bool, optional): Whether the new control is augmented. Defaults to False
58
+ """
59
+ if isinstance(other, Control):
60
+ super().append(other=other)
61
+ if not augmented:
62
+ self._true_dim += getattr(other, "_true_dim", other.shape[0])
63
+ self._update_slices()
64
+ else:
65
+ temp = Control(name=f"{self.name}_temp_append", shape=(1,))
66
+ temp.min = min
67
+ temp.max = max
68
+ temp.guess = guess
69
+ self.append(temp, augmented=augmented)
70
+
71
+ @property
72
+ def true(self):
73
+ """Get the true control variables (excluding augmented controls).
74
+
75
+ Returns:
76
+ Control: A new Control object containing only the true control variables
77
+ """
78
+ return self[self._true_slice]
79
+
80
+ @property
81
+ def augmented(self):
82
+ """Get the augmented control variables.
83
+
84
+ Returns:
85
+ Control: A new Control object containing only the augmented control variables
86
+ """
87
+ return self[self._augmented_slice]
88
+
89
+ def __getitem__(self, idx):
90
+ """Get a subset of the control variables.
91
+
92
+ Args:
93
+ idx: Index or slice to select control variables
94
+
95
+ Returns:
96
+ Control: A new Control object containing the selected variables
97
+ """
98
+ new_ctrl = super().__getitem__(idx)
99
+ new_ctrl.__class__ = Control
100
+
101
+ if isinstance(idx, slice):
102
+ selected = np.arange(self.shape[0])[idx]
103
+ elif isinstance(idx, (list, np.ndarray)):
104
+ selected = np.array(idx)
105
+ else:
106
+ selected = np.array([idx])
107
+
108
+ new_ctrl._true_dim = np.sum(selected < self._true_dim)
109
+ new_ctrl._update_slices()
110
+
111
+ return new_ctrl
112
+
113
+ def __repr__(self):
114
+ """String representation of the Control object.
115
+
116
+ Returns:
117
+ str: A string describing the Control object
118
+ """
119
+ return f"Control('{self.name}', shape={self.shape})"
@@ -0,0 +1,62 @@
1
+ import numpy as np
2
+
3
+
4
+ class Expr:
5
+ """
6
+ Base class for symbolic expressions in optimization problems.
7
+
8
+ Note: This class is currently not being used.
9
+ """
10
+
11
+ def __add__(self, other):
12
+ return Add(self, to_expr(other))
13
+
14
+ def __mul__(self, other):
15
+ return Mul(self, to_expr(other))
16
+
17
+ def __matmul__(self, other):
18
+ return MatMul(self, to_expr(other))
19
+
20
+ def __neg__(self):
21
+ return Neg(self)
22
+
23
+ def children(self):
24
+ return []
25
+
26
+ def pretty(self, indent=0):
27
+ pad = ' ' * indent
28
+ lines = [f"{pad}{self.__class__.__name__}"]
29
+ for child in self.children():
30
+ lines.append(child.pretty(indent + 1))
31
+ return '\n'.join(lines)
32
+
33
+
34
+ class Add(Expr):
35
+ def __init__(self, left, right):
36
+ self.left = left
37
+ self.right = right
38
+ def children(self):
39
+ return [self.left, self.right]
40
+
41
+
42
+ class Mul(Expr):
43
+ def __init__(self, left, right):
44
+ self.left = left
45
+ self.right = right
46
+ def children(self):
47
+ return [self.left, self.right]
48
+
49
+
50
+ class MatMul(Expr):
51
+ def __init__(self, left, right):
52
+ self.left = left
53
+ self.right = right
54
+ def children(self):
55
+ return [self.left, self.right]
56
+
57
+
58
+ class Neg(Expr):
59
+ def __init__(self, operand):
60
+ self.operand = operand
61
+ def children(self):
62
+ return [self.operand]
@@ -0,0 +1,95 @@
1
+ import numpy as np
2
+ from openscvx.backend.expr import Expr
3
+
4
+
5
+ class Parameter(Expr):
6
+ """A class representing a parameter in the optimization problem.
7
+
8
+ Parameters are symbolic variables that can be used in expressions and constraints.
9
+ They maintain a registry of all created parameters and can be indexed or sliced.
10
+
11
+ Attributes:
12
+ _registry (dict): Class-level dictionary storing all created parameters.
13
+ name (str): The name of the parameter.
14
+ _shape (tuple): The shape of the parameter.
15
+ value (any): The current value of the parameter, initially None.
16
+ """
17
+
18
+ _registry = {}
19
+
20
+ def __init__(self, name, shape=()):
21
+ """Initialize a new Parameter.
22
+
23
+ Args:
24
+ name (str): The name of the parameter.
25
+ shape (tuple, optional): The shape of the parameter. Defaults to ().
26
+
27
+ Note:
28
+ The parameter is automatically registered in the class registry if not already present.
29
+ """
30
+ super().__init__()
31
+ self.name = name
32
+ self._shape = shape
33
+ self.value = None
34
+
35
+ # Automatically register the parameter if not already present
36
+ if name not in Parameter._registry:
37
+ Parameter._registry[name] = self
38
+
39
+ @property
40
+ def shape(self):
41
+ """Get the shape of the parameter.
42
+
43
+ Returns:
44
+ tuple: The shape of the parameter.
45
+ """
46
+ return self._shape
47
+
48
+ def __getitem__(self, idx):
49
+ """Get a subset of the parameter using indexing or slicing.
50
+
51
+ Args:
52
+ idx (int or slice): The index or slice to use.
53
+ - If int: Returns a scalar parameter
54
+ - If slice: Returns a parameter with shape (length of slice,)
55
+
56
+ Returns:
57
+ Parameter: A new parameter representing the subset.
58
+
59
+ Raises:
60
+ TypeError: If idx is neither an int nor a slice.
61
+ """
62
+ if isinstance(idx, int):
63
+ param = Parameter(f"{self.name}[{idx}]", shape=())
64
+ elif isinstance(idx, slice):
65
+ length = len(range(*idx.indices(self.shape[0])))
66
+ param = Parameter(f"{self.name}[{idx.start}:{idx.stop}]", shape=(length,))
67
+ else:
68
+ raise TypeError("Parameter indices must be int or slice")
69
+
70
+ return param
71
+
72
+ def __repr__(self):
73
+ """Get a string representation of the parameter.
74
+
75
+ Returns:
76
+ str: A string showing the parameter name and shape.
77
+ """
78
+ return f"Parameter('{self.name}', shape={self.shape})"
79
+
80
+ @classmethod
81
+ def get_all(cls):
82
+ """Get all registered parameters.
83
+
84
+ Returns:
85
+ dict: A dictionary of all registered parameters, with names as keys.
86
+ """
87
+ return dict(cls._registry)
88
+
89
+ @classmethod
90
+ def reset(cls):
91
+ """Clear the registry of all parameters.
92
+
93
+ This method removes all registered parameters from the class registry.
94
+ """
95
+ cls._registry.clear()
@@ -0,0 +1,441 @@
1
+ import numpy as np
2
+
3
+ from openscvx.backend.variable import Variable
4
+
5
+
6
+ class Fix:
7
+ """Class representing a fixed state variable in the optimization problem.
8
+
9
+ A fixed state variable is one that is constrained to a specific value
10
+ and cannot be optimized.
11
+
12
+ Attributes:
13
+ value: The fixed value that the state variable must take.
14
+ """
15
+ def __init__(self, value):
16
+ """Initialize a new fixed state variable.
17
+
18
+ Args:
19
+ value: The fixed value that the state variable must take.
20
+ """
21
+ self.value = value
22
+
23
+ def __repr__(self):
24
+ """Get a string representation of this fixed state variable.
25
+
26
+ Returns:
27
+ str: A string representation showing the fixed value.
28
+ """
29
+ return f"Fix({self.value})"
30
+
31
+
32
+ class Free:
33
+ """Class representing a free state variable in the optimization problem.
34
+
35
+ A free state variable is one that is not constrained to any specific value
36
+ but can be optimized within its bounds.
37
+
38
+ Attributes:
39
+ guess: The initial guess value for optimization.
40
+ """
41
+ def __init__(self, guess):
42
+ """Initialize a new free state variable.
43
+
44
+ Args:
45
+ guess: The initial guess value for optimization.
46
+ """
47
+ self.guess = guess
48
+
49
+ def __repr__(self):
50
+ """Get a string representation of this free state variable.
51
+
52
+ Returns:
53
+ str: A string representation showing the guess value.
54
+ """
55
+ return f"Free({self.guess})"
56
+
57
+
58
+ class Minimize:
59
+ """Class representing a state variable to be minimized in the optimization problem.
60
+
61
+ A minimized state variable is one that is optimized to achieve the lowest
62
+ possible value within its bounds.
63
+
64
+ Attributes:
65
+ guess: The initial guess value for optimization.
66
+ """
67
+ def __init__(self, guess):
68
+ """Initialize a new minimized state variable.
69
+
70
+ Args:
71
+ guess: The initial guess value for optimization.
72
+ """
73
+ self.guess = guess
74
+
75
+ def __repr__(self):
76
+ """Get a string representation of this minimized state variable.
77
+
78
+ Returns:
79
+ str: A string representation showing the guess value.
80
+ """
81
+ return f"Minimize({self.guess})"
82
+
83
+
84
+ class Maximize:
85
+ """Class representing a state variable to be maximized in the optimization problem.
86
+
87
+ A maximized state variable is one that is optimized to achieve the highest
88
+ possible value within its bounds.
89
+
90
+ Attributes:
91
+ guess: The initial guess value for optimization.
92
+ """
93
+ def __init__(self, guess):
94
+ """Initialize a new maximized state variable.
95
+
96
+ Args:
97
+ guess: The initial guess value for optimization.
98
+ """
99
+ self.guess = guess
100
+
101
+ def __repr__(self):
102
+ """Get a string representation of this maximized state variable.
103
+
104
+ Returns:
105
+ str: A string representation showing the guess value.
106
+ """
107
+ return f"Maximize({self.guess})"
108
+
109
+ class State(Variable):
110
+ """A class representing the state variables in an optimal control problem.
111
+
112
+ The State class extends Variable to handle state-specific properties like initial and final conditions,
113
+ as well as true and augmented state dimensions. It supports various boundary condition types:
114
+ - Fixed values (Fix)
115
+ - Free variables (Free)
116
+ - Minimization objectives (Minimize)
117
+ - Maximization objectives (Maximize)
118
+
119
+ Attributes:
120
+ name (str): Name of the state variable.
121
+ shape (tuple): Shape of the state variable array.
122
+ min (np.ndarray): Minimum bounds for the state variables. Shape: (n_states,).
123
+ max (np.ndarray): Maximum bounds for the state variables. Shape: (n_states,).
124
+ guess (np.ndarray): Used to initialize SCP and contains the current SCP solution for the state trajectory. Shape: (n_nodes, n_states).
125
+ initial (np.ndarray): Initial state values or boundary condition objects (Free, Fixed, Minimize, Maximize). Shape: (n_states,).
126
+ final (np.ndarray): Final state values or boundary condition objects (Free, Fixed, Minimize, Maximize). Shape: (n_states,).
127
+ _initial (np.ndarray): Internal storage for initial state values.
128
+ _final (np.ndarray): Internal storage for final state values.
129
+ initial_type (str): Type of initial boundary condition ('fix', 'free', 'minimize', 'maximize').
130
+ final_type (str): Type of final boundary condition ('fix', 'free', 'minimize', 'maximize').
131
+ _true_dim (int): True dimensionality of the state variables.
132
+ _true_slice (slice): Slice for accessing true state variables.
133
+ _augmented_slice (slice): Slice for accessing augmented state variables.
134
+
135
+ Notes:
136
+ Attributes prefixed with underscore (_) are for internal use only and should not be accessed directly.
137
+
138
+ Example:
139
+ ```python
140
+ state = State("position", (3,))
141
+ state.min = np.array([0, 0, 10])
142
+ state.max = np.array([10, 10, 200])
143
+ state.guess = np.linspace([0, 1, 2], [10, 5, 8], 3)
144
+ state.initial = np.array([Fix(0), Free(1), 2])
145
+ state.final = np.array([Fix(10), Free(5), Maximize(8)])
146
+ ```
147
+ """
148
+
149
+ def __init__(self, name, shape):
150
+ """Initialize a State object.
151
+
152
+ Args:
153
+ name (str): Name identifier for the state variable
154
+ shape (tuple): Shape of the state vector
155
+ """
156
+ super().__init__(name, shape)
157
+ self._initial = None
158
+ self.initial_type = None
159
+ self._final = None
160
+ self.final_type = None
161
+
162
+ self._true_dim = shape[0]
163
+ self._update_slices()
164
+
165
+ def _update_slices(self):
166
+ """Update the slice objects for true and augmented states."""
167
+ self._true_slice = slice(0, self._true_dim)
168
+ self._augmented_slice = slice(self._true_dim, self.shape[0])
169
+
170
+ @property
171
+ def min(self):
172
+ """Get the minimum bounds for the state variables.
173
+
174
+ Returns:
175
+ np.ndarray: Array of minimum values for each state variable
176
+ """
177
+ return self._min
178
+
179
+ @min.setter
180
+ def min(self, val):
181
+ """Set the minimum bounds for the state variables.
182
+
183
+ Args:
184
+ val (np.ndarray): Array of minimum values for each state variable
185
+
186
+ Raises:
187
+ ValueError: If the shape of val doesn't match the state shape
188
+ """
189
+ val = np.asarray(val)
190
+ if val.shape != self.shape:
191
+ raise ValueError(f"Min shape {val.shape} does not match State shape {self.shape}")
192
+ self._min = val
193
+ self._check_bounds_against_initial_final()
194
+
195
+ @property
196
+ def max(self):
197
+ """Get the maximum bounds for the state variables.
198
+
199
+ Returns:
200
+ np.ndarray: Array of maximum values for each state variable
201
+ """
202
+ return self._max
203
+
204
+ @max.setter
205
+ def max(self, val):
206
+ """Set the maximum bounds for the state variables.
207
+
208
+ Args:
209
+ val (np.ndarray): Array of maximum values for each state variable
210
+
211
+ Raises:
212
+ ValueError: If the shape of val doesn't match the state shape
213
+ """
214
+ val = np.asarray(val)
215
+ if val.shape != self.shape:
216
+ raise ValueError(f"Max shape {val.shape} does not match State shape {self.shape}")
217
+ self._max = val
218
+ self._check_bounds_against_initial_final()
219
+
220
+ def _check_bounds_against_initial_final(self):
221
+ """Check if initial and final values respect the bounds.
222
+
223
+ Raises:
224
+ ValueError: If any fixed initial or final value violates the bounds
225
+ """
226
+ for field_name, data, types in [('initial', self._initial, self.initial_type),
227
+ ('final', self._final, self.final_type)]:
228
+ if data is None or types is None:
229
+ continue
230
+ for i, val in np.ndenumerate(data):
231
+ if types[i] != "Fix":
232
+ continue
233
+ min_i = self._min[i] if self._min is not None else -np.inf
234
+ max_i = self._max[i] if self._max is not None else np.inf
235
+ if val < min_i:
236
+ raise ValueError(f"{field_name.capitalize()} Fixed value at index {i[0]} is lower then the min: {val} < {min_i}")
237
+ if val > max_i:
238
+ raise ValueError(f"{field_name.capitalize()} Fixed value at index {i[0]} is greater then the max: {val} > {max_i}")
239
+
240
+ @property
241
+ def initial(self):
242
+ """Get the initial state values.
243
+
244
+ Returns:
245
+ np.ndarray: Array of initial state values
246
+ """
247
+ return self._initial
248
+
249
+ @initial.setter
250
+ def initial(self, arr):
251
+ """Set the initial state values and their types.
252
+
253
+ Args:
254
+ arr (np.ndarray): Array of initial values or boundary condition objects
255
+ (Fix, Free, Minimize, Maximize)
256
+
257
+ Raises:
258
+ ValueError: If the shape of arr doesn't match the state shape
259
+ """
260
+ arr = np.asarray(arr, dtype=object)
261
+ if arr.shape != self.shape:
262
+ raise ValueError(f"Initial value shape {arr.shape} does not match State shape {self.shape}")
263
+ self._initial = np.zeros(arr.shape)
264
+ self.initial_type = np.full(arr.shape, "Fix", dtype=object)
265
+
266
+ for i, v in np.ndenumerate(arr):
267
+ if isinstance(v, Free):
268
+ self._initial[i] = v.guess
269
+ self.initial_type[i] = "Free"
270
+ elif isinstance(v, Minimize):
271
+ self._initial[i] = v.guess
272
+ self.initial_type[i] = "Minimize"
273
+ elif isinstance(v, Maximize):
274
+ self._initial[i] = v.guess
275
+ self.initial_type[i] = "Maximize"
276
+ elif isinstance(v, Fix):
277
+ val = v.value
278
+ self._initial[i] = val
279
+ self.initial_type[i] = "Fix"
280
+ else:
281
+ val = v
282
+ self._initial[i] = val
283
+ self.initial_type[i] = "Fix"
284
+
285
+ self._check_bounds_against_initial_final()
286
+
287
+ @property
288
+ def final(self):
289
+ """Get the final state values.
290
+
291
+ Returns:
292
+ np.ndarray: Array of final state values
293
+ """
294
+ return self._final
295
+
296
+ @final.setter
297
+ def final(self, arr):
298
+ """Set the final state values and their types.
299
+
300
+ Args:
301
+ arr (np.ndarray): Array of final values or boundary condition objects
302
+ (Fix, Free, Minimize, Maximize)
303
+
304
+ Raises:
305
+ ValueError: If the shape of arr doesn't match the state shape
306
+ """
307
+ arr = np.asarray(arr, dtype=object)
308
+ if arr.shape != self.shape:
309
+ raise ValueError(f"Final value shape {arr.shape} does not match State shape {self.shape}")
310
+ self._final = np.zeros(arr.shape)
311
+ self.final_type = np.full(arr.shape, "Fix", dtype=object)
312
+
313
+ for i, v in np.ndenumerate(arr):
314
+ if isinstance(v, Free):
315
+ self._final[i] = v.guess
316
+ self.final_type[i] = "Free"
317
+ elif isinstance(v, Minimize):
318
+ self._final[i] = v.guess
319
+ self.final_type[i] = "Minimize"
320
+ elif isinstance(v, Maximize):
321
+ self._final[i] = v.guess
322
+ self.final_type[i] = "Maximize"
323
+ elif isinstance(v, Fix):
324
+ val = v.value
325
+ self._final[i] = val
326
+ self.final_type[i] = "Fix"
327
+ else:
328
+ val = v
329
+ self._final[i] = val
330
+ self.final_type[i] = "Fix"
331
+
332
+ self._check_bounds_against_initial_final()
333
+
334
+ @property
335
+ def true(self):
336
+ """Get the true state variables (excluding augmented states).
337
+
338
+ Returns:
339
+ State: A new State object containing only the true state variables
340
+ """
341
+ return self[self._true_slice]
342
+
343
+ @property
344
+ def augmented(self):
345
+ """Get the augmented state variables.
346
+
347
+ Returns:
348
+ State: A new State object containing only the augmented state variables
349
+ """
350
+ return self[self._augmented_slice]
351
+
352
+ def append(self, other=None, *, min=-np.inf, max=np.inf, guess=0.0, initial=0.0, final=0.0, augmented=False):
353
+ """Append another state or create a new state variable.
354
+
355
+ Args:
356
+ other (State, optional): Another State object to append
357
+ min (float, optional): Minimum bound for new state. Defaults to -np.inf
358
+ max (float, optional): Maximum bound for new state. Defaults to np.inf
359
+ guess (float, optional): Initial guess for new state. Defaults to 0.0
360
+ initial (float, optional): Initial value for new state. Defaults to 0.0
361
+ final (float, optional): Final value for new state. Defaults to 0.0
362
+ augmented (bool, optional): Whether the new state is augmented. Defaults to False
363
+ """
364
+ if isinstance(other, State):
365
+ super().append(other=other)
366
+
367
+ if self._initial is None:
368
+ self._initial = np.array(other._initial) if other._initial is not None else None
369
+ elif other._initial is not None:
370
+ self._initial = np.concatenate([self._initial, other._initial], axis=0)
371
+
372
+ if self._final is None:
373
+ self._final = np.array(other._final) if other._final is not None else None
374
+ elif other._final is not None:
375
+ self._final = np.concatenate([self._final, other._final], axis=0)
376
+
377
+ if self.initial_type is None:
378
+ self.initial_type = np.array(other.initial_type) if other.initial_type is not None else None
379
+ elif other.initial_type is not None:
380
+ self.initial_type = np.concatenate([self.initial_type, other.initial_type], axis=0)
381
+
382
+ if self.final_type is None:
383
+ self.final_type = np.array(other.final_type) if other.final_type is not None else None
384
+ elif other.final_type is not None:
385
+ self.final_type = np.concatenate([self.final_type, other.final_type], axis=0)
386
+
387
+ if not augmented:
388
+ self._true_dim += getattr(other, "_true_dim", other.shape[0])
389
+ self._update_slices()
390
+ else:
391
+ temp_state = State(name=f"{self.name}_temp_append", shape=(1,))
392
+ temp_state.min = min
393
+ temp_state.max = max
394
+ temp_state.guess = guess
395
+ temp_state.initial = initial
396
+ temp_state.final = final
397
+ self.append(temp_state, augmented=augmented)
398
+
399
+ def __getitem__(self, idx):
400
+ """Get a subset of the state variables.
401
+
402
+ Args:
403
+ idx: Index or slice to select state variables
404
+
405
+ Returns:
406
+ State: A new State object containing the selected variables
407
+ """
408
+ new_state = super().__getitem__(idx)
409
+ new_state.__class__ = State
410
+
411
+ def slice_attr(attr):
412
+ if attr is None:
413
+ return None
414
+ if attr.ndim == 2 and attr.shape[1] == self.shape[0]:
415
+ return attr[:, idx]
416
+ return attr[idx]
417
+
418
+ new_state._initial = slice_attr(self._initial)
419
+ new_state.initial_type = slice_attr(self.initial_type)
420
+ new_state._final = slice_attr(self._final)
421
+ new_state.final_type = slice_attr(self.final_type)
422
+
423
+ if isinstance(idx, slice):
424
+ selected = np.arange(self.shape[0])[idx]
425
+ elif isinstance(idx, (list, np.ndarray)):
426
+ selected = np.array(idx)
427
+ else:
428
+ selected = np.array([idx])
429
+
430
+ new_state._true_dim = np.sum(selected < self._true_dim)
431
+ new_state._update_slices()
432
+
433
+ return new_state
434
+
435
+ def __repr__(self):
436
+ """String representation of the State object.
437
+
438
+ Returns:
439
+ str: A string describing the State object
440
+ """
441
+ return f"State('{self.name}', shape={self.shape})"
@@ -0,0 +1,27 @@
1
+ # utils.py
2
+
3
+ from expr import Const, Add, Mul, MatMul, Neg
4
+
5
+ def pretty_print(expr):
6
+ print(expr.pretty())
7
+
8
+ def evaluate(expr, values):
9
+ if hasattr(expr, 'name'):
10
+ return values[expr.name]
11
+
12
+ if isinstance(expr, Const):
13
+ return expr.value
14
+
15
+ if isinstance(expr, Add):
16
+ return evaluate(expr.a, values) + evaluate(expr.b, values)
17
+
18
+ if isinstance(expr, Mul):
19
+ return evaluate(expr.a, values) * evaluate(expr.b, values)
20
+
21
+ if isinstance(expr, MatMul):
22
+ return evaluate(expr.a, values) @ evaluate(expr.b, values)
23
+
24
+ if isinstance(expr, Neg):
25
+ return -evaluate(expr.a, values)
26
+
27
+ raise NotImplementedError(f"Evaluation not implemented for {type(expr)}")
@@ -0,0 +1,209 @@
1
+ import numpy as np
2
+
3
+ from openscvx.backend.expr import Expr
4
+
5
+ class Variable(Expr):
6
+ """A base class for variables in an optimal control problem.
7
+
8
+ The Variable class provides the fundamental structure for state and control variables,
9
+ handling their shapes, bounds, and initial guesses. It supports operations like
10
+ appending new variables and slicing.
11
+
12
+ Attributes:
13
+ name (str): Name identifier for the variable
14
+ _shape (tuple): Shape of the variable vector
15
+ _min (np.ndarray): Minimum bounds for the variable
16
+ _max (np.ndarray): Maximum bounds for the variable
17
+ _guess (np.ndarray): Initial guess for the variable trajectory
18
+
19
+ """
20
+
21
+ def __init__(self, name, shape):
22
+ """Initialize a Variable object.
23
+
24
+ Args:
25
+ name (str): Name identifier for the variable
26
+ shape (tuple): Shape of the variable vector
27
+ """
28
+ super().__init__()
29
+ self.name = name
30
+ self._shape = shape
31
+ self._min = None
32
+ self._max = None
33
+ self._guess = None
34
+
35
+ @property
36
+ def shape(self):
37
+ """Get the shape of the variable.
38
+
39
+ Returns:
40
+ tuple: Shape of the variable vector
41
+ """
42
+ return self._shape
43
+
44
+ @property
45
+ def min(self):
46
+ """Get the minimum bounds for the variable.
47
+
48
+ Returns:
49
+ np.ndarray: Array of minimum values for each variable
50
+ """
51
+ return self._min
52
+
53
+ @min.setter
54
+ def min(self, arr):
55
+ """Set the minimum bounds for the variable.
56
+
57
+ Args:
58
+ arr (np.ndarray): Array of minimum values for each variable
59
+
60
+ Raises:
61
+ ValueError: If the shape of arr doesn't match the variable shape
62
+ """
63
+ arr = np.asarray(arr, dtype=float)
64
+ if arr.ndim != 1 or arr.shape[0] != self.shape[0]:
65
+ raise ValueError(f"{self.__class__.__name__} min must be 1D with shape ({self.shape[0]},), got {arr.shape}")
66
+ self._min = arr
67
+
68
+ @property
69
+ def max(self):
70
+ """Get the maximum bounds for the variable.
71
+
72
+ Returns:
73
+ np.ndarray: Array of maximum values for each variable
74
+ """
75
+ return self._max
76
+
77
+ @max.setter
78
+ def max(self, arr):
79
+ """Set the maximum bounds for the variable.
80
+
81
+ Args:
82
+ arr (np.ndarray): Array of maximum values for each variable
83
+
84
+ Raises:
85
+ ValueError: If the shape of arr doesn't match the variable shape
86
+ """
87
+ arr = np.asarray(arr, dtype=float)
88
+ if arr.ndim != 1 or arr.shape[0] != self.shape[0]:
89
+ raise ValueError(f"{self.__class__.__name__} max must be 1D with shape ({self.shape[0]},), got {arr.shape}")
90
+ self._max = arr
91
+
92
+ @property
93
+ def guess(self):
94
+ """Get the initial guess for the variable trajectory.
95
+
96
+ Returns:
97
+ np.ndarray: Array of initial guesses for each variable at each time point
98
+ """
99
+ return self._guess
100
+
101
+ @guess.setter
102
+ def guess(self, arr):
103
+ """Set the initial guess for the variable trajectory.
104
+
105
+ Args:
106
+ arr (np.ndarray): 2D array of initial guesses with shape (n_guess_points, n_variables)
107
+
108
+ Raises:
109
+ ValueError: If the shape of arr doesn't match the expected dimensions
110
+ """
111
+ arr = np.asarray(arr)
112
+ if arr.ndim != 2:
113
+ raise ValueError(f"Guess must be a 2D array of shape (n_guess_points, {self.shape[0]}), got shape {arr.shape}")
114
+ if arr.shape[1] != self.shape[0]:
115
+ raise ValueError(f"Guess must have second dimension equal to variable dimension {self.shape[0]}, got {arr.shape[1]}")
116
+ self._guess = arr
117
+
118
+ def append(self, other=None, *, min=-np.inf, max=np.inf, guess=0.0):
119
+ """Append another variable or create a new variable.
120
+
121
+ Args:
122
+ other (Variable, optional): Another Variable object to append
123
+ min (float, optional): Minimum bound for new variable. Defaults to -np.inf
124
+ max (float, optional): Maximum bound for new variable. Defaults to np.inf
125
+ guess (float, optional): Initial guess for new variable. Defaults to 0.0
126
+ """
127
+ def process_array(val, is_guess=False):
128
+ """Process input array to ensure correct shape and type.
129
+
130
+ Args:
131
+ val: Input value to process
132
+ is_guess (bool): Whether the value is a guess array
133
+
134
+ Returns:
135
+ np.ndarray: Processed array with correct shape and type
136
+ """
137
+ arr = np.asarray(val, dtype=float)
138
+ if is_guess:
139
+ return np.atleast_2d(arr)
140
+ return np.atleast_1d(arr)
141
+
142
+ if isinstance(other, Variable):
143
+ self._shape = (self.shape[0] + other.shape[0],)
144
+
145
+ if self._min is not None and other._min is not None:
146
+ self._min = np.concatenate([self._min, process_array(other._min)], axis=0)
147
+
148
+ if self._max is not None and other._max is not None:
149
+ self._max = np.concatenate([self._max, process_array(other._max)], axis=0)
150
+
151
+ if self._guess is not None and other._guess is not None:
152
+ self._guess = np.concatenate([self._guess, process_array(other._guess, is_guess=True)], axis=1)
153
+
154
+ else:
155
+ self._shape = (self.shape[0] + 1,)
156
+
157
+ if self._min is not None:
158
+ self._min = np.concatenate([self._min, process_array(min)], axis=0)
159
+
160
+ if self._max is not None:
161
+ self._max = np.concatenate([self._max, process_array(max)], axis=0)
162
+
163
+ if self._guess is not None:
164
+ guess_arr = process_array(guess, is_guess=True)
165
+ if guess_arr.shape[1] != 1:
166
+ guess_arr = guess_arr.T
167
+ self._guess = np.concatenate([self._guess, guess_arr], axis=1)
168
+
169
+ def __getitem__(self, idx):
170
+ """Get a subset of the variable.
171
+
172
+ Args:
173
+ idx (int or slice): Index or slice to select variables
174
+
175
+ Returns:
176
+ Variable: A new Variable object containing the selected variables
177
+
178
+ Raises:
179
+ TypeError: If idx is not an int or slice
180
+ """
181
+ if isinstance(idx, int):
182
+ new_shape = ()
183
+ elif isinstance(idx, slice):
184
+ new_shape = (len(range(*idx.indices(self.shape[0]))),)
185
+ else:
186
+ raise TypeError("Variable indices must be int or slice")
187
+
188
+ sliced = Variable(f"{self.name}[{idx}]", new_shape)
189
+
190
+ def slice_attr(attr):
191
+ """Slice an attribute array based on the index.
192
+
193
+ Args:
194
+ attr (np.ndarray): Attribute array to slice
195
+
196
+ Returns:
197
+ np.ndarray: Sliced attribute array
198
+ """
199
+ if attr is None:
200
+ return None
201
+ if attr.ndim == 2 and attr.shape[1] == self.shape[0]:
202
+ return attr[:, idx]
203
+ return attr[idx]
204
+
205
+ sliced._min = slice_attr(self._min)
206
+ sliced._max = slice_attr(self._max)
207
+ sliced._guess = slice_attr(self._guess)
208
+
209
+ return sliced
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openscvx
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A general Python-based successive convexification implementation which uses a JAX backend.
5
5
  Home-page: https://haynec.github.io/openscvx/
6
6
  Author: Chris Hayner and Griffin Norris
@@ -1,5 +1,5 @@
1
1
  openscvx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- openscvx/_version.py,sha256=UoNvMtd4wCG76RwoSpNCUtaFyTwakGcZolfjXzNVSMY,511
2
+ openscvx/_version.py,sha256=OjGGK5TcHVG44Y62aAqeJH4CskkZoY9ydbHOtCDew50,511
3
3
  openscvx/config.py,sha256=BsMAZUWCBUMl43nvakQ2OOdf2iszsFv9VP27J4xDXEs,16106
4
4
  openscvx/discretization.py,sha256=FBpJKoVTXYAxeE6PvaekbRFbLWeKY6EJfl214ib2tX8,7908
5
5
  openscvx/dynamics.py,sha256=TQosmwDXpWvR22-ZL2JawDDjVkyoL-DN45Xuv7ecCnc,6417
@@ -16,12 +16,19 @@ openscvx/utils.py,sha256=CEd_kjh-MUhRInZjoxogUjMddU5ExJ-IAOXq_Hn4KGY,4166
16
16
  openscvx/augmentation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  openscvx/augmentation/ctcs.py,sha256=m1jdALXSqHq3WD6lCBAUI7FR0Sfs8aCYr66h0EwE4z4,1707
18
18
  openscvx/augmentation/dynamics_augmentation.py,sha256=4lEeSRl3LJxvcclwGSI1Yp21kc_oKoIVga08uzaKXE0,4702
19
+ openscvx/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ openscvx/backend/control.py,sha256=vOAZNzVZy6-sft5OzP6ZavQiaaDTLSJhjVJHvgPH9Vo,4457
21
+ openscvx/backend/expr.py,sha256=GQfnQxizogUONQ4oBtSQZMlFQYesunBbT95n1EMAPRo,1374
22
+ openscvx/backend/parameter.py,sha256=WmH_MiruQ8rSdf_mS0RfRYJLVgIRU-nlA794GMrbOLU,3047
23
+ openscvx/backend/state.py,sha256=UbyerDHm_BTE1T5vI1HFYvBXxDwBl-ocdY2knJrJOGA,15952
24
+ openscvx/backend/utils.py,sha256=pxh-YG1pSkcxzaPe9Lx8bkmwcd-RSCs0mdaaZNMg4CE,707
25
+ openscvx/backend/variable.py,sha256=AqyLc13PKMm_eK1SDnqns6Fkx7hQdG--r5dnEBUfMf8,7104
19
26
  openscvx/constraints/__init__.py,sha256=J3_UyJMUVYzvhjfyBM_m9WIRDlHQV7Q4CiI-TzRapAs,165
20
27
  openscvx/constraints/ctcs.py,sha256=-VJZE8O0xq8n9mzSiDNikax-xo6CpWJtjwMbAPzu1-8,10121
21
28
  openscvx/constraints/nodal.py,sha256=qT_U8Go0n0rzv4MupwQYGL8H5P8ShA7ZGATJ7IHnIqU,8185
22
29
  openscvx/constraints/violation.py,sha256=curg7R6ER4YtKzdqmd1tgS2kWICGon5ab_Y4YGbeibw,2875
23
- openscvx-0.2.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
24
- openscvx-0.2.1.dist-info/METADATA,sha256=2SUNu4T25V27sB60rayMSyHFozYtFb-ObDDbGszsybw,9052
25
- openscvx-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- openscvx-0.2.1.dist-info/top_level.txt,sha256=nUT4Ybefzh40H8tVXqc1RzKESy_MAowElb-CIvAbd4Q,9
27
- openscvx-0.2.1.dist-info/RECORD,,
30
+ openscvx-0.2.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
31
+ openscvx-0.2.2.dist-info/METADATA,sha256=EBYY6LZeEvcvaId2WQEne9tNtnSk0IyMszpXK3-2vfE,9052
32
+ openscvx-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ openscvx-0.2.2.dist-info/top_level.txt,sha256=nUT4Ybefzh40H8tVXqc1RzKESy_MAowElb-CIvAbd4Q,9
34
+ openscvx-0.2.2.dist-info/RECORD,,