Classes and Immutability¶
This document describes the consistent pattern of immutability and lazy caching across the following core data and geometry classes in the Ptera Software codebase:
UnsteadyProblemMovementAirplaneMovementWingMovementWingCrossSectionMovementOperatingPointMovementSteadyProblemOperatingPointAirplaneWingWingCrossSectionAirfoilPanelRingVortexHorseshoeVortexLineVortex
Design Principles¶
Class Attribute Categories¶
Most attribute falls into one of these categories:
Category |
Pattern |
|---|---|
Immutable |
Read-only property (no setter), set once in |
Derived (Immutable) |
Manual lazy caching (check |
Set Once |
Property with setter that raises |
Mutable |
Property with setter, or plain attribute |
Derived (Set Once) |
Manual lazy caching, depends on set once attributes |
Key Decisions¶
No cache invalidation for immutable/set once attributes: Since these are only set once, we don’t need invalidation logic in setters.
Enforce set once semantics at runtime: Set once properties raise
AttributeErrorif assigned a second time. This catches bugs early where code incorrectly attempts to modify values that should be immutable after initial assignment.Use manual lazy caching for all derived properties: This approach:
Works consistently for properties derived from both immutable and set once attributes
Is compatible with
__slots__(no dependency on__dict__)Simplifies
__deepcopy__(cache variables can be copied directly)Maintains the existing pattern already used throughout the codebase
Solver mutable attributes remain mutable: Properties that the solver needs to set keep their setters.
__slots__on every class: All classes in the package define__slots__, which eliminates per-instance__dict__overhead and prevents accidental dynamic attribute assignment. This catches typos likeself.num_panles = 5at runtime with anAttributeErrorinstead of silently creating a new attribute.
NumPy Array Mutability¶
Even with read-only properties, numpy arrays can still be mutated in place via the getter (e.g., panel.Frpp_G_Cg[0] = 999.0). To prevent this, all numpy arrays that should be immutable are set to read-only using arr.flags.writeable = False:
Immutable arrays: Set in
__init__immediately after assignmentSet once arrays: Set in the setter after assignment
Derived cached arrays: Set in the lazy property after computation (since numpy operations like subtraction create new writable arrays regardless of input writability)
Deepcopy: Use
.copy()then setflags.writeable = Falseon the copy
Deepcopy Cache Handling¶
When implementing __deepcopy__, handle cached derived properties based on their source:
Derived from Immutable -> Preserve: Copy the cached values (they remain valid since the source immutable attributes are also copied). For numpy arrays, use
.copy()then setflags.writeable = False.Derived from Set Once -> Reset to None: These depend on values that will be set fresh by the solver or meshing, so reset them.
Serialization Attribute Handling¶
The _serialization module uses the same object.__new__() + object.__setattr__() pattern as the __deepcopy__ methods but with a simpler strategy: the logical state of each object is preserved exactly as it was at save time, including all cached values on primary slots. For these primary attributes, no values are reset to None during deserialization.
Some redundant or alias slots (for example, solver alias slots and Movement._airplanes / Movement._operating_points) are treated specially: they are serialized as null and then deterministically rebuilt from their canonical sources during deserialization. This preserves object graph identity and avoids duplicating equivalent objects while still yielding the same effective state as the original instance.
This works because:
Deserialized objects are fully formed snapshots. There is no subsequent
__init__, solver run, or meshing step that would populate set once attributes, so there is no conflict with set once guards.Cached derived values remain valid because the immutable and set once attributes they depend on are also restored with their original values, and any alias slots are rebuilt to match.
NumPy array writeable flags are preserved through serialization, maintaining the same mutability guarantees as the original objects.
When adding or renaming __slots__ on any class, both __deepcopy__ and _serialization are affected. The serialization module discovers attributes generically via __slots__, so new slots are automatically serialized (subject to the intentional omission of redundant/alias slots described above). However, adding or removing slots requires incrementing _FORMAT_VERSION in _serialization.py to ensure old files are not loaded with incompatible code.
List Collection Immutability¶
Store collections as tuples internally to prevent external mutation via .append(), .pop(), etc.
UnsteadyProblem Class (problems.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Movement definition |
|
|
Results flag |
|
|
Copied from Movement |
|
|
Copied from Movement |
|
|
Computed during init |
|
|
Computed during init |
|
|
Copied from Movement |
|
|
Generated during init |
Mutable (populated by solver)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Final forces |
|
|
Final force coefficients |
|
|
Final moments |
|
|
Final moment coefficients |
|
|
Cycle averaged forces |
|
|
Cycle averaged coefficients |
|
|
Cycle averaged moments |
|
|
Cycle averaged moment coefficients |
|
|
RMS forces |
|
|
RMS force coefficients |
|
|
RMS moments |
|
|
RMS moment coefficients |
Note: The solver result lists must remain mutable as they are populated after initialization by the solver. These are initialized as empty lists and appended to during the solve.
Movement Class (movements/movement.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Tuple prevents mutation |
|
|
Operating point changes |
|
|
Time step |
|
|
Number of cycles |
|
|
Number of chord lengths |
|
|
Total time steps |
|
|
Max wake rows per Wing |
|
|
Max wake in chord lengths |
|
|
Max wake in motion cycles |
|
|
Generated Airplanes |
|
|
Generated OperatingPoints |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Cached |
|
|
Cached |
|
|
Cached |
|
|
Cached |
Note on airplanes and operating_points: These are generated during __init__ by calling the child movements’ generate_* methods. Are stored as nested tuples to prevent modification after generation.
AirplaneMovement Class (movements/airplane_movement.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Base geometry |
|
|
Tuple prevents mutation |
|
|
CG position amplitudes |
|
|
CG position periods |
|
|
CG position spacing |
|
|
CG position phases |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
Own periods + child |
Tuple of unique non zero periods (cached) |
|
Own periods + child |
Scalar float, longest period (cached) |
WingMovement Class (movements/wing_movement.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Base geometry |
|
|
Tuple prevents mutation |
|
|
Position amplitudes |
|
|
Position periods |
|
|
Position spacing |
|
|
Position phases |
|
|
Angle amplitudes |
|
|
Angle periods |
|
|
Angle spacing |
|
|
Angle phases |
|
|
Rotation point offset |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
Own periods + child |
Tuple of unique non zero periods (cached) |
|
Own periods + child |
Scalar float, longest period (cached) |
WingCrossSectionMovement Class (movements/wing_cross_section_movement.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Base geometry |
|
|
Position amplitudes |
|
|
Position periods |
|
|
Position spacing |
|
|
Position phases |
|
|
Angle amplitudes |
|
|
Angle periods |
|
|
Angle spacing |
|
|
Angle phases |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
Period arrays |
Tuple of unique non zero periods (cached) |
|
Period arrays |
Scalar float, longest period (cached) |
OperatingPointMovement Class (movements/operating_point_movement.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Base operating conditions |
|
|
Amplitude |
|
|
Period |
|
|
Spacing function |
|
|
Phase offset |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Cached |
SteadyProblem Class (problems.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Tuple prevents external mutation |
|
|
Operating conditions |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Tuple of Re for each Airplane (cached) |
OperatingPoint Class (operating_point.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Fluid density |
|
|
CG speed |
|
|
Angle of attack |
|
|
Sideslip angle |
|
|
Earth-to-body orientation |
|
|
CG position in Earth axes |
|
|
Image surface normal |
|
|
Image surface point |
|
|
External force |
|
|
Kinematic viscosity |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Dynamic pressure (cached) |
|
(constant) |
Geometry-to-body matrix (cached) |
|
|
Inverse of above (cached) |
|
|
Body-to-wind matrix (cached) |
|
|
Inverse of above (cached) |
|
|
Geometry-to-wind matrix (cached) |
|
|
Inverse of above (cached) |
|
|
Earth-to-body matrix (cached) |
|
|
Inverse of above (cached) |
|
|
Earth-to-geometry matrix (cached) |
|
|
Inverse of above (cached) |
|
|
Surface normal in GP1 (cached) |
|
|
Surface point in GP1 (cached) |
|
|
Active reflection matrix (cached) |
|
|
Freestream direction (cached) |
|
|
Freestream velocity (cached) |
Note on caching: While vInfHat_GP1__E and vInf_GP1__E are simple computations once the transformation matrix is cached, they are cached for consistency with the overall pattern and because they are called repeatedly during solver operations. The same is true for qInf__E.
Note on T_pas_GP1_CgP1_to_BP1_CgP1: This matrix is a constant (180-degree rotation about y) and does not depend on any __init__ parameters. It is still lazily cached for consistency with the overall pattern and to avoid recomputing it on every access.
Note on transformation decomposition: The geometry-to-wind transformation (T_pas_GP1_CgP1_to_W_CgP1) is composed from T_pas_GP1_CgP1_to_BP1_CgP1 and T_pas_BP1_CgP1_to_W_CgP1 via compose_T_pas. Similarly, T_pas_E_CgP1_to_GP1_CgP1 is composed from T_pas_E_CgP1_to_BP1_CgP1 and T_pas_BP1_CgP1_to_GP1_CgP1. Decomposing through body axes enables the Earth transformation chain to reuse the geometry-to-body and body-to-geometry matrices without duplication.
Airplane Class (geometry/airplane.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Processed for symmetry during init (tuple prevents mutation) |
|
|
Airplane identifier |
|
|
CG position in formation coordinates |
|
|
Aircraft weight in Newtons |
|
|
Reference wetted area |
|
|
Reference chord length |
|
|
Reference span |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Sum of wing panel counts |
|
|
Transformation matrix |
Mutable (set by solver)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Forces in wind axes |
|
|
Force coefficients |
|
|
Moments in wind axes |
|
|
Moment coefficients |
Wing Class (geometry/wing.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Wing cross sections (tuple prevents mutation) |
|
|
Wing identifier |
|
|
Leading edge root position |
|
|
Rotation angles |
|
|
Chordwise panel count |
|
|
“cosine” or “uniform” |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
Immutable attrs |
Transformation matrix |
|
Above |
Inverse transformation |
|
Above |
Basis vectors |
|
Cross sections |
Child transformations |
Set Once (set by generate_mesh, never modified after)¶
Attribute |
Type |
Set By |
Notes |
|---|---|---|---|
|
|
|
1, 2, 3, or 4 |
|
|
|
Total spanwise count |
|
|
|
Total panel count |
|
|
|
Panel matrix |
Derived from Set Once (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Projected area |
|
|
Wetted area |
|
|
Average aspect ratio |
|
Wing cross sections, |
Wing span |
|
|
Standard mean chord |
|
|
MAC |
Note on caching: Most derived properties iterate over panels or wing cross sections. For large meshes, caching projected_area, wetted_area, and span provides meaningful performance gains if they’re accessed multiple times. Since their source attributes are immutable or set once, these are cached after first computation without invalidation logic.
Mutable (modified by process_wing_symmetry for type 5 symmetry)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Modified to False for type 5 |
|
|
Modified to False for type 5 |
|
|
Modified to None for type 5 |
|
|
Modified to None for type 5 |
Note: These are modified by Airplane.process_wing_symmetry() when type 5 symmetry is detected. The original Wing becomes a type 1 wing and a new reflected Wing is created with type 3 symmetry.
Mutable (modified during simulation for wake)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Wake vortex array, grows |
|
|
Wake vortex positions, grows |
WingCrossSection Class (geometry/wing_cross_section.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Wing cross section airfoil |
|
|
Spanwise panel count |
|
|
Chord length |
|
|
Position in parent axes |
|
|
Rotation angles |
|
|
Hinge location (0-1) |
|
|
Deflection in degrees |
|
|
“cosine” or “uniform” |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Transformation matrix |
|
Above |
Inverse transformation |
Set Once (set by parent Wing)¶
Attribute |
Type |
Set By |
Notes |
|---|---|---|---|
|
|
|
Validation flag |
|
|
|
Inherited symmetry type |
Mutable (modified by process_wing_symmetry)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Set to None for type 5 symmetry |
Note: Modified at when type 5 symmetry is split into two wings.
Airfoil Class (geometry/airfoil.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Airfoil identifier |
|
|
Outline coordinates |
|
|
Resampling flag |
|
|
Points per side for resampling |
|
|
Mean camber line coordinates |
Note: The add_control_surface method creates and returns a new Airfoil instance rather than modifying the existing one. This is the correct immutable pattern.
Panel Class (_panel.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Front right corner (geometry axes) |
|
|
Front left corner (geometry axes) |
|
|
Back left corner (geometry axes) |
|
|
Back right corner (geometry axes) |
|
|
Edge flag |
|
|
Edge flag |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Right leg vector |
|
|
Front leg vector |
|
|
Left leg vector |
|
|
Back leg vector |
|
|
Front right bound vortex point |
|
|
Front left bound vortex point |
|
Multiple corners |
Collocation point |
|
All corners |
Unit normal vector |
|
All corners |
Panel area |
|
All legs |
Aspect ratio |
Set Once (set after construction by meshing or Problem)¶
Attribute |
Type |
Set By |
Notes |
|---|---|---|---|
|
|
Meshing |
Edge flag |
|
|
Meshing |
Edge flag |
|
|
Meshing |
Grid position |
|
|
Meshing |
Grid position |
|
|
Problem |
Front right (formation axes) |
|
|
Problem |
Front left (formation axes) |
|
|
Problem |
Back left (formation axes) |
|
|
Problem |
Back right (formation axes) |
Derived from Set Once (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Right leg (formation axes) |
|
|
Front leg (formation axes) |
|
|
Left leg (formation axes) |
|
|
Back leg (formation axes) |
|
|
Front right BVP (formation) |
|
|
Front left BVP (formation) |
|
Multiple GP1 corners |
Collocation point (formation) |
|
All GP1 corners |
Unit normal (formation) |
Mutable (set by solver)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Attached vortex |
|
|
Attached vortex |
|
|
Computed forces |
|
|
Computed moments |
|
|
Forces in wind axes |
|
|
Moments in wind axes |
RingVortex Class (_vortices/ring_vortex.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Front right corner |
|
|
Front left corner |
|
|
Back left corner |
|
|
Back right corner |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
All corners |
Centroid position |
|
All corners |
Vortex area |
Mutable (solver sets these)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Vortex strength (solver finds this) |
|
|
Age in simulation time (incremented for wake) |
Derived (special: child objects)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Child |
|
|
Child |
|
|
Child |
|
|
Child |
Note on legs: The leg LineVortex objects depend on both geometry (immutable) and strength (mutable). Since strength is set by the solver AFTER the vortex is created, we propagate strength updates to existing legs. Since legs are accessed repeatedly during induced velocity calculations, option 2 (keeping the current propagation) is more efficient.
HorseshoeVortex Class (_vortices/horseshoe_vortex.py)¶
Attribute Classification¶
Immutable (set in __init__, never modified)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Front right point |
|
|
Front left point |
|
|
Direction of left leg (normalized) |
|
|
Length of semi-infinite legs |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Back right point |
|
|
Back left point |
Mutable (solver sets these)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Vortex strength |
Derived (child objects)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Child |
|
|
Child |
|
|
Child |
LineVortex Class (_vortices/_line_vortex.py)¶
Attribute Classification¶
Since LineVortex is an internal class whose endpoints ARE updated by parent vortex classes when their corners change, we need to consider whether this mutation actually happens.
Immutable (set in __init__)¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Start point |
|
|
End point |
Derived from Immutable (use manual lazy caching)¶
Property |
Depends On |
Notes |
|---|---|---|
|
|
Line vector |
|
|
Center point |
Mutable¶
Attribute |
Type |
Notes |
|---|---|---|
|
|
Updated by parent vortex |
Solver Classes (Not Covered Above)¶
The three solver classes (SteadyHorseshoeVortexLatticeMethodSolver, SteadyRingVortexLatticeMethodSolver, and UnsteadyRingVortexLatticeMethodSolver) are intentionally omitted from the immutability and lazy caching patterns described in this document. Unlike the data and geometry classes above, the solver classes are algorithmic classes whose attributes are internal mutable working state in a procedural computation pipeline. They are not shared data that external code accesses or modifies, so immutable properties, set once enforcement, and lazy caching would add significant boilerplate with no meaningful safety benefit. The solver classes do still use __slots__, like all other classes in the package, to protect against dynamic attribute assignment typos.