openscvx 0.3.2.dev170__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.

Files changed (79) hide show
  1. openscvx/__init__.py +123 -0
  2. openscvx/_version.py +34 -0
  3. openscvx/algorithms/__init__.py +92 -0
  4. openscvx/algorithms/autotuning.py +24 -0
  5. openscvx/algorithms/base.py +351 -0
  6. openscvx/algorithms/optimization_results.py +215 -0
  7. openscvx/algorithms/penalized_trust_region.py +384 -0
  8. openscvx/config.py +437 -0
  9. openscvx/discretization/__init__.py +47 -0
  10. openscvx/discretization/discretization.py +236 -0
  11. openscvx/expert/__init__.py +23 -0
  12. openscvx/expert/byof.py +326 -0
  13. openscvx/expert/lowering.py +419 -0
  14. openscvx/expert/validation.py +357 -0
  15. openscvx/integrators/__init__.py +48 -0
  16. openscvx/integrators/runge_kutta.py +281 -0
  17. openscvx/lowered/__init__.py +30 -0
  18. openscvx/lowered/cvxpy_constraints.py +23 -0
  19. openscvx/lowered/cvxpy_variables.py +124 -0
  20. openscvx/lowered/dynamics.py +34 -0
  21. openscvx/lowered/jax_constraints.py +133 -0
  22. openscvx/lowered/parameters.py +54 -0
  23. openscvx/lowered/problem.py +70 -0
  24. openscvx/lowered/unified.py +718 -0
  25. openscvx/plotting/__init__.py +63 -0
  26. openscvx/plotting/plotting.py +756 -0
  27. openscvx/plotting/scp_iteration.py +299 -0
  28. openscvx/plotting/viser/__init__.py +126 -0
  29. openscvx/plotting/viser/animated.py +605 -0
  30. openscvx/plotting/viser/plotly_integration.py +333 -0
  31. openscvx/plotting/viser/primitives.py +355 -0
  32. openscvx/plotting/viser/scp.py +459 -0
  33. openscvx/plotting/viser/server.py +112 -0
  34. openscvx/problem.py +734 -0
  35. openscvx/propagation/__init__.py +60 -0
  36. openscvx/propagation/post_processing.py +104 -0
  37. openscvx/propagation/propagation.py +248 -0
  38. openscvx/solvers/__init__.py +51 -0
  39. openscvx/solvers/cvxpy.py +226 -0
  40. openscvx/symbolic/__init__.py +9 -0
  41. openscvx/symbolic/augmentation.py +630 -0
  42. openscvx/symbolic/builder.py +492 -0
  43. openscvx/symbolic/constraint_set.py +92 -0
  44. openscvx/symbolic/expr/__init__.py +222 -0
  45. openscvx/symbolic/expr/arithmetic.py +517 -0
  46. openscvx/symbolic/expr/array.py +632 -0
  47. openscvx/symbolic/expr/constraint.py +796 -0
  48. openscvx/symbolic/expr/control.py +135 -0
  49. openscvx/symbolic/expr/expr.py +720 -0
  50. openscvx/symbolic/expr/lie/__init__.py +87 -0
  51. openscvx/symbolic/expr/lie/adjoint.py +357 -0
  52. openscvx/symbolic/expr/lie/se3.py +172 -0
  53. openscvx/symbolic/expr/lie/so3.py +138 -0
  54. openscvx/symbolic/expr/linalg.py +279 -0
  55. openscvx/symbolic/expr/math.py +699 -0
  56. openscvx/symbolic/expr/spatial.py +209 -0
  57. openscvx/symbolic/expr/state.py +607 -0
  58. openscvx/symbolic/expr/stl.py +136 -0
  59. openscvx/symbolic/expr/variable.py +321 -0
  60. openscvx/symbolic/hashing.py +112 -0
  61. openscvx/symbolic/lower.py +760 -0
  62. openscvx/symbolic/lowerers/__init__.py +106 -0
  63. openscvx/symbolic/lowerers/cvxpy.py +1302 -0
  64. openscvx/symbolic/lowerers/jax.py +1382 -0
  65. openscvx/symbolic/preprocessing.py +757 -0
  66. openscvx/symbolic/problem.py +110 -0
  67. openscvx/symbolic/time.py +116 -0
  68. openscvx/symbolic/unified.py +420 -0
  69. openscvx/utils/__init__.py +20 -0
  70. openscvx/utils/cache.py +131 -0
  71. openscvx/utils/caching.py +210 -0
  72. openscvx/utils/printing.py +301 -0
  73. openscvx/utils/profiling.py +37 -0
  74. openscvx/utils/utils.py +100 -0
  75. openscvx-0.3.2.dev170.dist-info/METADATA +350 -0
  76. openscvx-0.3.2.dev170.dist-info/RECORD +79 -0
  77. openscvx-0.3.2.dev170.dist-info/WHEEL +5 -0
  78. openscvx-0.3.2.dev170.dist-info/licenses/LICENSE +201 -0
  79. openscvx-0.3.2.dev170.dist-info/top_level.txt +1 -0
@@ -0,0 +1,607 @@
1
+ import hashlib
2
+ from enum import Enum
3
+
4
+ import numpy as np
5
+
6
+ from .variable import Variable
7
+
8
+
9
+ class BoundaryType(str, Enum):
10
+ """Enumeration of boundary condition types for state variables.
11
+
12
+ This enum allows users to specify boundary conditions using plain strings
13
+ while maintaining type safety internally. Boundary conditions control how
14
+ the optimizer handles initial and final state values.
15
+
16
+ Attributes:
17
+ FIXED (str): State value is fixed to a specific value
18
+ FREE (str): State value is free to be optimized within bounds
19
+ MINIMIZE (str): Objective term to minimize the state value
20
+ MAXIMIZE (str): Objective term to maximize the state value
21
+
22
+ Example:
23
+ Can use either enum or string:
24
+
25
+ BoundaryType.FIXED
26
+ "fixed" # Equivalent
27
+ """
28
+
29
+ FIXED = "fixed"
30
+ FREE = "free"
31
+ MINIMIZE = "minimize"
32
+ MAXIMIZE = "maximize"
33
+
34
+
35
+ def Free(guess):
36
+ """Create a free boundary condition tuple.
37
+
38
+ This is a convenience function that returns a tuple ("free", guess) which
39
+ can be used to specify free boundary conditions for State or Time objects.
40
+
41
+ Args:
42
+ guess: Initial guess value for the free variable.
43
+
44
+ Returns:
45
+ tuple: ("free", guess) tuple suitable for use in State.initial, State.final,
46
+ or Time.initial, Time.final.
47
+
48
+ Example:
49
+ ```python
50
+ pos = ox.State("pos", (3,))
51
+ pos.final = [ox.Free(5.0), ox.Free(3.0), 10] # First two free, third fixed
52
+
53
+ time = ox.Time(
54
+ initial=0.0,
55
+ final=ox.Free(10.0),
56
+ min=0.0,
57
+ max=20.0
58
+ )
59
+ ```
60
+ """
61
+ return ("free", guess)
62
+
63
+
64
+ def Fixed(value):
65
+ """Create a fixed boundary condition tuple.
66
+
67
+ This is a convenience function that returns a tuple ("fixed", value) which
68
+ can be used to explicitly specify fixed boundary conditions for State or Time objects.
69
+ Note that plain numbers default to fixed, so this is mainly for clarity.
70
+
71
+ Args:
72
+ value: Fixed value for the boundary condition.
73
+
74
+ Returns:
75
+ tuple: ("fixed", value) tuple suitable for use in State.initial, State.final,
76
+ or Time.initial, Time.final.
77
+
78
+ Example:
79
+ ```python
80
+ pos = ox.State("pos", (3,))
81
+ pos.final = [ox.Fixed(10.0), ox.Free(5.0), ox.Fixed(2.0)]
82
+
83
+ # Equivalent to:
84
+ pos.final = [10.0, ox.Free(5.0), 2.0] # Plain numbers default to fixed
85
+ ```
86
+ """
87
+ return ("fixed", value)
88
+
89
+
90
+ def Minimize(guess):
91
+ """Create a minimize boundary condition tuple.
92
+
93
+ This is a convenience function that returns a tuple ("minimize", guess) which
94
+ can be used to specify that a boundary value should be minimized in the objective
95
+ function for State or Time objects.
96
+
97
+ Args:
98
+ guess: Initial guess value for the variable to be minimized.
99
+
100
+ Returns:
101
+ tuple: ("minimize", guess) tuple suitable for use in State.initial, State.final,
102
+ or Time.initial, Time.final.
103
+
104
+ Example:
105
+ ```python
106
+ time = ox.Time(
107
+ initial=0.0,
108
+ final=ox.Minimize(10.0), # Minimize final time
109
+ min=0.0,
110
+ max=20.0
111
+ )
112
+
113
+ fuel = ox.State("fuel", (1,))
114
+ fuel.final = [ox.Minimize(0)] # Minimize final fuel consumption
115
+ ```
116
+ """
117
+ return ("minimize", guess)
118
+
119
+
120
+ def Maximize(guess):
121
+ """Create a maximize boundary condition tuple.
122
+
123
+ This is a convenience function that returns a tuple ("maximize", guess) which
124
+ can be used to specify that a boundary value should be maximized in the objective
125
+ function for State or Time objects.
126
+
127
+ Args:
128
+ guess: Initial guess value for the variable to be maximized.
129
+
130
+ Returns:
131
+ tuple: ("maximize", guess) tuple suitable for use in State.initial, State.final,
132
+ or Time.initial, Time.final.
133
+
134
+ Example:
135
+ ```python
136
+ altitude = ox.State("altitude", (1,))
137
+ altitude.final = [ox.Maximize(100.0)] # Maximize final altitude
138
+
139
+ time = ox.Time(
140
+ initial=ox.Maximize(0.0), # Maximize initial time
141
+ final=10.0,
142
+ min=0.0,
143
+ max=20.0
144
+ )
145
+ ```
146
+ """
147
+ return ("maximize", guess)
148
+
149
+
150
+ class State(Variable):
151
+ """State variable with boundary conditions for trajectory optimization.
152
+
153
+ State represents a dynamic state variable in a trajectory optimization problem.
154
+ Unlike control inputs, states evolve according to dynamics constraints and can
155
+ have boundary conditions specified at the initial and final time points.
156
+ Like all Variables, States also support min/max bounds and initial trajectory
157
+ guesses to help guide the optimization solver toward good solutions.
158
+
159
+ States support four types of boundary conditions:
160
+
161
+ - **fixed**: State value is constrained to a specific value
162
+ - **free**: State value is optimized within the specified bounds
163
+ - **minimize**: Adds a term to the objective function to minimize the state value
164
+ - **maximize**: Adds a term to the objective function to maximize the state value
165
+
166
+ Each element of a multi-dimensional state can have different boundary condition
167
+ types, allowing for fine-grained control over the optimization.
168
+
169
+ Attributes:
170
+ name (str): Unique name identifier for this state variable
171
+ _shape (tuple[int, ...]): Shape of the state vector (typically 1D like (3,) for 3D position)
172
+ _slice (slice | None): Internal slice information for variable indexing
173
+ _min (np.ndarray | None): Minimum bounds for state variables
174
+ _max (np.ndarray | None): Maximum bounds for state variables
175
+ _guess (np.ndarray | None): Initial trajectory guess
176
+ _initial (np.ndarray | None): Initial state values with boundary condition types
177
+ initial_type (np.ndarray | None): Array of boundary condition types for initial state
178
+ _final (np.ndarray | None): Final state values with boundary condition types
179
+ final_type (np.ndarray | None): Array of boundary condition types for final state
180
+
181
+ Example:
182
+ Scalar time state with fixed initial time, minimize final time:
183
+
184
+ time = State("time", (1,))
185
+ time.min = [0.0]
186
+ time.max = [10.0]
187
+ time.initial = [("fixed", 0.0)]
188
+ time.final = [("minimize", 5.0)]
189
+
190
+ 3D position state with mixed boundary conditions:
191
+
192
+ pos = State("pos", (3,))
193
+ pos.min = [0, 0, 10]
194
+ pos.max = [10, 10, 200]
195
+ pos.initial = [0, ("free", 1), 50] # x fixed, y free, z fixed
196
+ pos.final = [10, ("free", 5), ("maximize", 150)] # Maximize final altitude
197
+ """
198
+
199
+ def __init__(self, name, shape):
200
+ """Initialize a State object.
201
+
202
+ Args:
203
+ name: Name identifier for the state variable
204
+ shape: Shape of the state vector (typically 1D tuple)
205
+ """
206
+ super().__init__(name, shape)
207
+ self._initial = None
208
+ self.initial_type = None
209
+ self._final = None
210
+ self.final_type = None
211
+ self._scaling_min = None
212
+ self._scaling_max = None
213
+
214
+ def _hash_into(self, hasher: "hashlib._Hash") -> None:
215
+ """Hash State including boundary condition types.
216
+
217
+ Extends Variable._hash_into to include the structural metadata that
218
+ affects the compiled problem: boundary condition types (fixed, free,
219
+ minimize, maximize). Values are not hashed as they are runtime parameters.
220
+
221
+ Args:
222
+ hasher: A hashlib hash object to update
223
+ """
224
+ # Hash the base Variable attributes (class name, shape, slice)
225
+ super()._hash_into(hasher)
226
+ # Hash boundary condition types (these affect constraint structure)
227
+ if self.initial_type is not None:
228
+ hasher.update(b"initial_type:")
229
+ hasher.update(str(self.initial_type.tolist()).encode())
230
+ if self.final_type is not None:
231
+ hasher.update(b"final_type:")
232
+ hasher.update(str(self.final_type.tolist()).encode())
233
+
234
+ @property
235
+ def min(self):
236
+ """Get the minimum bounds for the state variables.
237
+
238
+ Returns:
239
+ Array of minimum values for each state variable element.
240
+
241
+ Example:
242
+ Get lower bounds:
243
+
244
+ pos = State("pos", (3,))
245
+ pos.min = [0, 0, 10]
246
+ print(pos.min) # [0. 0. 10.]
247
+ """
248
+ return self._min
249
+
250
+ @min.setter
251
+ def min(self, val):
252
+ """Set the minimum bounds for the state variables.
253
+
254
+ Bounds are validated against any fixed initial/final conditions to ensure
255
+ consistency.
256
+
257
+ Args:
258
+ val: Array of minimum values, must match the state shape exactly
259
+
260
+ Raises:
261
+ ValueError: If the shape doesn't match the state shape, or if fixed
262
+ boundary conditions violate the bounds
263
+
264
+ Example:
265
+ Set lower bounds:
266
+
267
+ pos = State("pos", (3,))
268
+ pos.min = [0, 0, 10]
269
+ pos.initial = [0, 5, 15] # Must satisfy: 0>=0, 5>=0, 15>=10
270
+ """
271
+ val = np.asarray(val, dtype=float)
272
+ if val.shape != self.shape:
273
+ raise ValueError(f"Min shape {val.shape} does not match State shape {self.shape}")
274
+ self._min = val
275
+ self._check_bounds_against_initial_final()
276
+
277
+ @property
278
+ def max(self):
279
+ """Get the maximum bounds for the state variables.
280
+
281
+ Returns:
282
+ Array of maximum values for each state variable element.
283
+
284
+ Example:
285
+ Get upper bounds:
286
+
287
+ vel = State("vel", (3,))
288
+ vel.max = [10, 10, 5]
289
+ print(vel.max) # [10. 10. 5.]
290
+ """
291
+ return self._max
292
+
293
+ @max.setter
294
+ def max(self, val):
295
+ """Set the maximum bounds for the state variables.
296
+
297
+ Bounds are validated against any fixed initial/final conditions to ensure
298
+ consistency.
299
+
300
+ Args:
301
+ val: Array of maximum values, must match the state shape exactly
302
+
303
+ Raises:
304
+ ValueError: If the shape doesn't match the state shape, or if fixed
305
+ boundary conditions violate the bounds
306
+
307
+ Example:
308
+ Set upper bounds:
309
+
310
+ vel = State("vel", (3,))
311
+ vel.max = [10, 10, 5]
312
+ vel.final = [8, 9, 4] # Must satisfy: 8<=10, 9<=10, 4<=5
313
+ """
314
+ val = np.asarray(val, dtype=float)
315
+ if val.shape != self.shape:
316
+ raise ValueError(f"Max shape {val.shape} does not match State shape {self.shape}")
317
+ self._max = val
318
+ self._check_bounds_against_initial_final()
319
+
320
+ def _check_bounds_against_initial_final(self):
321
+ """Validate that fixed boundary conditions respect min/max bounds.
322
+
323
+ This internal method is automatically called when bounds or boundary
324
+ conditions are set to ensure consistency.
325
+
326
+ Raises:
327
+ ValueError: If any fixed initial or final value violates the min/max bounds
328
+ """
329
+ for field_name, data, types in [
330
+ ("initial", self._initial, self.initial_type),
331
+ ("final", self._final, self.final_type),
332
+ ]:
333
+ if data is None or types is None:
334
+ continue
335
+ for i, val in np.ndenumerate(data):
336
+ if types[i] != "Fix":
337
+ continue
338
+ min_i = self._min[i] if self._min is not None else -np.inf
339
+ max_i = self._max[i] if self._max is not None else np.inf
340
+ if val < min_i:
341
+ raise ValueError(
342
+ f"{field_name.capitalize()} Fixed value at index {i[0]} is lower then the "
343
+ f"min: {val} < {min_i}"
344
+ )
345
+ if val > max_i:
346
+ raise ValueError(
347
+ f"{field_name.capitalize()} Fixed value at index {i[0]} is greater then "
348
+ f"the max: {val} > {max_i}"
349
+ )
350
+
351
+ @property
352
+ def initial(self):
353
+ """Get the initial state boundary condition values.
354
+
355
+ Returns:
356
+ Array of initial state values (regardless of boundary condition type),
357
+ or None if not set.
358
+
359
+ Note:
360
+ Use `initial_type` to see the boundary condition types for each element.
361
+
362
+ Example:
363
+ Get initial state boundary conditions:
364
+
365
+ x = State("x", (2,))
366
+ x.initial = [0, ("free", 1)]
367
+ print(x.initial) # [0. 1.]
368
+ print(x.initial_type) # ['Fix' 'Free']
369
+ """
370
+ return self._initial
371
+
372
+ @initial.setter
373
+ def initial(self, arr):
374
+ """Set the initial state boundary conditions.
375
+
376
+ Each element can be specified as either a simple number (defaults to "fixed")
377
+ or a tuple of (type, value) where type specifies the boundary condition.
378
+
379
+ Args:
380
+ arr: Array-like of initial conditions. Each element can be:
381
+ - A number: Defaults to fixed boundary condition at that value
382
+ - A tuple (type, value): Where type is one of:
383
+ - "fixed": Constrain state to this exact value
384
+ - "free": Let optimizer choose within bounds, initialize at value
385
+ - "minimize": Add objective term to minimize, initialize at value
386
+ - "maximize": Add objective term to maximize, initialize at value
387
+
388
+ Raises:
389
+ ValueError: If the shape doesn't match the state shape, if boundary
390
+ condition type is invalid, or if fixed values violate bounds
391
+
392
+ Example:
393
+ Set initial state boundary conditions:
394
+
395
+ pos = State("pos", (3,))
396
+ pos.min = [0, 0, 0]
397
+ pos.max = [10, 10, 10]
398
+ # x fixed at 0, y free (starts at 5), z fixed at 2
399
+ pos.initial = [0, ("free", 5), 2]
400
+
401
+ Can also minimize/maximize boundary values:
402
+
403
+ time = State("t", (1,))
404
+ time.initial = [("minimize", 0)] # Minimize initial time
405
+ """
406
+ # Convert to list first to handle mixed types properly
407
+ if not isinstance(arr, (list, tuple)):
408
+ arr = np.asarray(arr)
409
+ if arr.shape != self.shape:
410
+ raise ValueError(f"Shape mismatch: {arr.shape} != {self.shape}")
411
+ arr = arr.tolist()
412
+
413
+ # Ensure we have the right number of elements
414
+ if len(arr) != self.shape[0]:
415
+ raise ValueError(f"Length mismatch: got {len(arr)} elements, expected {self.shape[0]}")
416
+
417
+ self._initial = np.zeros(self.shape, dtype=float)
418
+ self.initial_type = np.full(self.shape, "Fix", dtype=object)
419
+
420
+ for i, v in enumerate(arr):
421
+ if isinstance(v, tuple) and len(v) == 2:
422
+ # Tuple API: (type, value)
423
+ bc_type_str, bc_value = v
424
+ try:
425
+ bc_type = BoundaryType(bc_type_str) # Validates the string
426
+ except ValueError:
427
+ valid_types = [t.value for t in BoundaryType]
428
+ raise ValueError(
429
+ f"Invalid boundary condition type: {bc_type_str}. "
430
+ f"Valid types are: {valid_types}"
431
+ )
432
+ self._initial[i] = float(bc_value)
433
+ self.initial_type[i] = bc_type.value.capitalize()
434
+ elif isinstance(v, (int, float, np.number)):
435
+ # Simple number defaults to fixed
436
+ self._initial[i] = float(v)
437
+ self.initial_type[i] = "Fix"
438
+ else:
439
+ raise ValueError(
440
+ f"Invalid boundary condition format: {v}. "
441
+ f"Use a number (defaults to fixed) or tuple ('type', value) "
442
+ f"where type is 'fixed', 'free', 'minimize', or 'maximize'."
443
+ )
444
+
445
+ self._check_bounds_against_initial_final()
446
+
447
+ @property
448
+ def final(self):
449
+ """Get the final state boundary condition values.
450
+
451
+ Returns:
452
+ Array of final state values (regardless of boundary condition type),
453
+ or None if not set.
454
+
455
+ Note:
456
+ Use `final_type` to see the boundary condition types for each element.
457
+
458
+ Example:
459
+ Get final state boundary conditions:
460
+
461
+ x = State("x", (2,))
462
+ x.final = [10, ("minimize", 0)]
463
+ print(x.final) # [10. 0.]
464
+ print(x.final_type) # ['Fix' 'Minimize']
465
+ """
466
+ return self._final
467
+
468
+ @final.setter
469
+ def final(self, arr):
470
+ """Set the final state boundary conditions.
471
+
472
+ Each element can be specified as either a simple number (defaults to "fixed")
473
+ or a tuple of (type, value) where type specifies the boundary condition.
474
+
475
+ Args:
476
+ arr: Array-like of final conditions. Each element can be:
477
+ - A number: Defaults to fixed boundary condition at that value
478
+ - A tuple (type, value): Where type is one of:
479
+ - "fixed": Constrain state to this exact value
480
+ - "free": Let optimizer choose within bounds, initialize at value
481
+ - "minimize": Add objective term to minimize, initialize at value
482
+ - "maximize": Add objective term to maximize, initialize at value
483
+
484
+ Raises:
485
+ ValueError: If the shape doesn't match the state shape, if boundary
486
+ condition type is invalid, or if fixed values violate bounds
487
+
488
+ Example:
489
+ Set final state boundary conditionis:
490
+
491
+ pos = State("pos", (3,))
492
+ pos.min = [0, 0, 0]
493
+ pos.max = [10, 10, 10]
494
+ # x fixed at 10, y free (starts at 5), z maximize altitude
495
+ pos.final = [10, ("free", 5), ("maximize", 8)]
496
+
497
+ Minimize final time in time-optimal problem:
498
+
499
+ time = State("t", (1,))
500
+ time.final = [("minimize", 10)]
501
+ """
502
+ # Convert to list first to handle mixed types properly
503
+ if not isinstance(arr, (list, tuple)):
504
+ arr = np.asarray(arr)
505
+ if arr.shape != self.shape:
506
+ raise ValueError(f"Shape mismatch: {arr.shape} != {self.shape}")
507
+ arr = arr.tolist()
508
+
509
+ # Ensure we have the right number of elements
510
+ if len(arr) != self.shape[0]:
511
+ raise ValueError(f"Length mismatch: got {len(arr)} elements, expected {self.shape[0]}")
512
+
513
+ self._final = np.zeros(self.shape, dtype=float)
514
+ self.final_type = np.full(self.shape, "Fix", dtype=object)
515
+
516
+ for i, v in enumerate(arr):
517
+ if isinstance(v, tuple) and len(v) == 2:
518
+ # Tuple API: (type, value)
519
+ bc_type_str, bc_value = v
520
+ try:
521
+ bc_type = BoundaryType(bc_type_str) # Validates the string
522
+ except ValueError:
523
+ valid_types = [t.value for t in BoundaryType]
524
+ raise ValueError(
525
+ f"Invalid boundary condition type: {bc_type_str}. "
526
+ f"Valid types are: {valid_types}"
527
+ )
528
+ self._final[i] = float(bc_value)
529
+ self.final_type[i] = bc_type.value.capitalize()
530
+ elif isinstance(v, (int, float, np.number)):
531
+ # Simple number defaults to fixed
532
+ self._final[i] = float(v)
533
+ self.final_type[i] = "Fix"
534
+ else:
535
+ raise ValueError(
536
+ f"Invalid boundary condition format: {v}. "
537
+ f"Use a number (defaults to fixed) or tuple ('type', value) "
538
+ f"where type is 'fixed', 'free', 'minimize', or 'maximize'."
539
+ )
540
+
541
+ self._check_bounds_against_initial_final()
542
+
543
+ @property
544
+ def scaling_min(self):
545
+ """Get the scaling minimum bounds for the state variables.
546
+
547
+ Returns:
548
+ Array of scaling minimum values for each state variable element, or None if not set.
549
+ """
550
+ return self._scaling_min
551
+
552
+ @scaling_min.setter
553
+ def scaling_min(self, val):
554
+ """Set the scaling minimum bounds for the state variables.
555
+
556
+ Args:
557
+ val: Array of scaling minimum values, must match the state shape exactly
558
+
559
+ Raises:
560
+ ValueError: If the shape doesn't match the state shape
561
+ """
562
+ if val is None:
563
+ self._scaling_min = None
564
+ return
565
+ val = np.asarray(val, dtype=float)
566
+ if val.shape != self.shape:
567
+ raise ValueError(
568
+ f"Scaling min shape {val.shape} does not match State shape {self.shape}"
569
+ )
570
+ self._scaling_min = val
571
+
572
+ @property
573
+ def scaling_max(self):
574
+ """Get the scaling maximum bounds for the state variables.
575
+
576
+ Returns:
577
+ Array of scaling maximum values for each state variable element, or None if not set.
578
+ """
579
+ return self._scaling_max
580
+
581
+ @scaling_max.setter
582
+ def scaling_max(self, val):
583
+ """Set the scaling maximum bounds for the state variables.
584
+
585
+ Args:
586
+ val: Array of scaling maximum values, must match the state shape exactly
587
+
588
+ Raises:
589
+ ValueError: If the shape doesn't match the state shape
590
+ """
591
+ if val is None:
592
+ self._scaling_max = None
593
+ return
594
+ val = np.asarray(val, dtype=float)
595
+ if val.shape != self.shape:
596
+ raise ValueError(
597
+ f"Scaling max shape {val.shape} does not match State shape {self.shape}"
598
+ )
599
+ self._scaling_max = val
600
+
601
+ def __repr__(self):
602
+ """String representation of the State object.
603
+
604
+ Returns:
605
+ Concise string showing the state name and shape.
606
+ """
607
+ return f"State('{self.name}', shape={self.shape})"